##// END OF EJS Templates
patch: git metadata was ignored if strip > 1...
Patrick Mezard -
r14385:7709cc98 default
parent child Browse files
Show More
@@ -1,1778 +1,1776 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):
481 class workingbackend(fsbackend):
482 def __init__(self, ui, repo, similarity):
482 def __init__(self, ui, repo, similarity):
483 super(workingbackend, self).__init__(ui, repo.root)
483 super(workingbackend, self).__init__(ui, repo.root)
484 self.repo = repo
484 self.repo = repo
485 self.similarity = similarity
485 self.similarity = similarity
486 self.removed = set()
486 self.removed = set()
487 self.changed = set()
487 self.changed = set()
488 self.copied = []
488 self.copied = []
489
489
490 def writelines(self, fname, lines, mode):
490 def writelines(self, fname, lines, mode):
491 super(workingbackend, self).writelines(fname, lines, mode)
491 super(workingbackend, self).writelines(fname, lines, mode)
492 self.changed.add(fname)
492 self.changed.add(fname)
493
493
494 def unlink(self, fname):
494 def unlink(self, fname):
495 super(workingbackend, self).unlink(fname)
495 super(workingbackend, self).unlink(fname)
496 self.removed.add(fname)
496 self.removed.add(fname)
497 self.changed.add(fname)
497 self.changed.add(fname)
498
498
499 def copy(self, src, dst):
499 def copy(self, src, dst):
500 super(workingbackend, self).copy(src, dst)
500 super(workingbackend, self).copy(src, dst)
501 self.copied.append((src, dst))
501 self.copied.append((src, dst))
502 self.changed.add(dst)
502 self.changed.add(dst)
503
503
504 def setmode(self, fname, islink, isexec):
504 def setmode(self, fname, islink, isexec):
505 super(workingbackend, self).setmode(fname, islink, isexec)
505 super(workingbackend, self).setmode(fname, islink, isexec)
506 self.changed.add(fname)
506 self.changed.add(fname)
507
507
508 def close(self):
508 def close(self):
509 wctx = self.repo[None]
509 wctx = self.repo[None]
510 addremoved = set(self.changed)
510 addremoved = set(self.changed)
511 for src, dst in self.copied:
511 for src, dst in self.copied:
512 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
512 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
513 addremoved.discard(src)
513 addremoved.discard(src)
514 if (not self.similarity) and self.removed:
514 if (not self.similarity) and self.removed:
515 wctx.remove(sorted(self.removed))
515 wctx.remove(sorted(self.removed))
516 if addremoved:
516 if addremoved:
517 cwd = self.repo.getcwd()
517 cwd = self.repo.getcwd()
518 if cwd:
518 if cwd:
519 addremoved = [util.pathto(self.repo.root, cwd, f)
519 addremoved = [util.pathto(self.repo.root, cwd, f)
520 for f in addremoved]
520 for f in addremoved]
521 scmutil.addremove(self.repo, addremoved, similarity=self.similarity)
521 scmutil.addremove(self.repo, addremoved, similarity=self.similarity)
522 return sorted(self.changed)
522 return sorted(self.changed)
523
523
524 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
524 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
525 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
525 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
526 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
526 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
527 eolmodes = ['strict', 'crlf', 'lf', 'auto']
527 eolmodes = ['strict', 'crlf', 'lf', 'auto']
528
528
529 class patchfile(object):
529 class patchfile(object):
530 def __init__(self, ui, fname, backend, mode, missing=False,
530 def __init__(self, ui, fname, backend, mode, missing=False,
531 eolmode='strict'):
531 eolmode='strict'):
532 self.fname = fname
532 self.fname = fname
533 self.eolmode = eolmode
533 self.eolmode = eolmode
534 self.eol = None
534 self.eol = None
535 self.backend = backend
535 self.backend = backend
536 self.ui = ui
536 self.ui = ui
537 self.lines = []
537 self.lines = []
538 self.exists = False
538 self.exists = False
539 self.missing = missing
539 self.missing = missing
540 self.mode = mode
540 self.mode = mode
541 if not missing:
541 if not missing:
542 try:
542 try:
543 self.lines = self.backend.readlines(fname)
543 self.lines = self.backend.readlines(fname)
544 if self.lines:
544 if self.lines:
545 # Normalize line endings
545 # Normalize line endings
546 if self.lines[0].endswith('\r\n'):
546 if self.lines[0].endswith('\r\n'):
547 self.eol = '\r\n'
547 self.eol = '\r\n'
548 elif self.lines[0].endswith('\n'):
548 elif self.lines[0].endswith('\n'):
549 self.eol = '\n'
549 self.eol = '\n'
550 if eolmode != 'strict':
550 if eolmode != 'strict':
551 nlines = []
551 nlines = []
552 for l in self.lines:
552 for l in self.lines:
553 if l.endswith('\r\n'):
553 if l.endswith('\r\n'):
554 l = l[:-2] + '\n'
554 l = l[:-2] + '\n'
555 nlines.append(l)
555 nlines.append(l)
556 self.lines = nlines
556 self.lines = nlines
557 self.exists = True
557 self.exists = True
558 except IOError:
558 except IOError:
559 pass
559 pass
560 else:
560 else:
561 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)
562
562
563 self.hash = {}
563 self.hash = {}
564 self.dirty = 0
564 self.dirty = 0
565 self.offset = 0
565 self.offset = 0
566 self.skew = 0
566 self.skew = 0
567 self.rej = []
567 self.rej = []
568 self.fileprinted = False
568 self.fileprinted = False
569 self.printfile(False)
569 self.printfile(False)
570 self.hunks = 0
570 self.hunks = 0
571
571
572 def writelines(self, fname, lines, mode):
572 def writelines(self, fname, lines, mode):
573 if self.eolmode == 'auto':
573 if self.eolmode == 'auto':
574 eol = self.eol
574 eol = self.eol
575 elif self.eolmode == 'crlf':
575 elif self.eolmode == 'crlf':
576 eol = '\r\n'
576 eol = '\r\n'
577 else:
577 else:
578 eol = '\n'
578 eol = '\n'
579
579
580 if self.eolmode != 'strict' and eol and eol != '\n':
580 if self.eolmode != 'strict' and eol and eol != '\n':
581 rawlines = []
581 rawlines = []
582 for l in lines:
582 for l in lines:
583 if l and l[-1] == '\n':
583 if l and l[-1] == '\n':
584 l = l[:-1] + eol
584 l = l[:-1] + eol
585 rawlines.append(l)
585 rawlines.append(l)
586 lines = rawlines
586 lines = rawlines
587
587
588 self.backend.writelines(fname, lines, mode)
588 self.backend.writelines(fname, lines, mode)
589
589
590 def printfile(self, warn):
590 def printfile(self, warn):
591 if self.fileprinted:
591 if self.fileprinted:
592 return
592 return
593 if warn or self.ui.verbose:
593 if warn or self.ui.verbose:
594 self.fileprinted = True
594 self.fileprinted = True
595 s = _("patching file %s\n") % self.fname
595 s = _("patching file %s\n") % self.fname
596 if warn:
596 if warn:
597 self.ui.warn(s)
597 self.ui.warn(s)
598 else:
598 else:
599 self.ui.note(s)
599 self.ui.note(s)
600
600
601
601
602 def findlines(self, l, linenum):
602 def findlines(self, l, linenum):
603 # looks through the hash and finds candidate lines. The
603 # looks through the hash and finds candidate lines. The
604 # result is a list of line numbers sorted based on distance
604 # result is a list of line numbers sorted based on distance
605 # from linenum
605 # from linenum
606
606
607 cand = self.hash.get(l, [])
607 cand = self.hash.get(l, [])
608 if len(cand) > 1:
608 if len(cand) > 1:
609 # resort our list of potentials forward then back.
609 # resort our list of potentials forward then back.
610 cand.sort(key=lambda x: abs(x - linenum))
610 cand.sort(key=lambda x: abs(x - linenum))
611 return cand
611 return cand
612
612
613 def write_rej(self):
613 def write_rej(self):
614 # our rejects are a little different from patch(1). This always
614 # our rejects are a little different from patch(1). This always
615 # 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
616 # 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
617 # without having to type the filename.
617 # without having to type the filename.
618 if not self.rej:
618 if not self.rej:
619 return
619 return
620 base = os.path.basename(self.fname)
620 base = os.path.basename(self.fname)
621 lines = ["--- %s\n+++ %s\n" % (base, base)]
621 lines = ["--- %s\n+++ %s\n" % (base, base)]
622 for x in self.rej:
622 for x in self.rej:
623 for l in x.hunk:
623 for l in x.hunk:
624 lines.append(l)
624 lines.append(l)
625 if l[-1] != '\n':
625 if l[-1] != '\n':
626 lines.append("\n\ No newline at end of file\n")
626 lines.append("\n\ No newline at end of file\n")
627 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
627 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
628
628
629 def apply(self, h):
629 def apply(self, h):
630 if not h.complete():
630 if not h.complete():
631 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
631 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
632 (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),
633 h.lenb))
633 h.lenb))
634
634
635 self.hunks += 1
635 self.hunks += 1
636
636
637 if self.missing:
637 if self.missing:
638 self.rej.append(h)
638 self.rej.append(h)
639 return -1
639 return -1
640
640
641 if self.exists and h.createfile():
641 if self.exists and h.createfile():
642 self.ui.warn(_("file %s already exists\n") % self.fname)
642 self.ui.warn(_("file %s already exists\n") % self.fname)
643 self.rej.append(h)
643 self.rej.append(h)
644 return -1
644 return -1
645
645
646 if isinstance(h, binhunk):
646 if isinstance(h, binhunk):
647 if h.rmfile():
647 if h.rmfile():
648 self.backend.unlink(self.fname)
648 self.backend.unlink(self.fname)
649 else:
649 else:
650 self.lines[:] = h.new()
650 self.lines[:] = h.new()
651 self.offset += len(h.new())
651 self.offset += len(h.new())
652 self.dirty = True
652 self.dirty = True
653 return 0
653 return 0
654
654
655 horig = h
655 horig = h
656 if (self.eolmode in ('crlf', 'lf')
656 if (self.eolmode in ('crlf', 'lf')
657 or self.eolmode == 'auto' and self.eol):
657 or self.eolmode == 'auto' and self.eol):
658 # If new eols are going to be normalized, then normalize
658 # If new eols are going to be normalized, then normalize
659 # hunk data before patching. Otherwise, preserve input
659 # hunk data before patching. Otherwise, preserve input
660 # line-endings.
660 # line-endings.
661 h = h.getnormalized()
661 h = h.getnormalized()
662
662
663 # fast case first, no offsets, no fuzz
663 # fast case first, no offsets, no fuzz
664 old = h.old()
664 old = h.old()
665 # patch starts counting at 1 unless we are adding the file
665 # patch starts counting at 1 unless we are adding the file
666 if h.starta == 0:
666 if h.starta == 0:
667 start = 0
667 start = 0
668 else:
668 else:
669 start = h.starta + self.offset - 1
669 start = h.starta + self.offset - 1
670 orig_start = start
670 orig_start = start
671 # 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
672 # when the hunk cleanly applies at start + skew, so skip the
672 # when the hunk cleanly applies at start + skew, so skip the
673 # fast case code
673 # fast case code
674 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:
675 if h.rmfile():
675 if h.rmfile():
676 self.backend.unlink(self.fname)
676 self.backend.unlink(self.fname)
677 else:
677 else:
678 self.lines[start : start + h.lena] = h.new()
678 self.lines[start : start + h.lena] = h.new()
679 self.offset += h.lenb - h.lena
679 self.offset += h.lenb - h.lena
680 self.dirty = True
680 self.dirty = True
681 return 0
681 return 0
682
682
683 # 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
684 self.hash = {}
684 self.hash = {}
685 for x, s in enumerate(self.lines):
685 for x, s in enumerate(self.lines):
686 self.hash.setdefault(s, []).append(x)
686 self.hash.setdefault(s, []).append(x)
687 if h.hunk[-1][0] != ' ':
687 if h.hunk[-1][0] != ' ':
688 # 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
689 # override the start line and use eof here
689 # override the start line and use eof here
690 search_start = len(self.lines)
690 search_start = len(self.lines)
691 else:
691 else:
692 search_start = orig_start + self.skew
692 search_start = orig_start + self.skew
693
693
694 for fuzzlen in xrange(3):
694 for fuzzlen in xrange(3):
695 for toponly in [True, False]:
695 for toponly in [True, False]:
696 old = h.old(fuzzlen, toponly)
696 old = h.old(fuzzlen, toponly)
697
697
698 cand = self.findlines(old[0][1:], search_start)
698 cand = self.findlines(old[0][1:], search_start)
699 for l in cand:
699 for l in cand:
700 if diffhelpers.testhunk(old, self.lines, l) == 0:
700 if diffhelpers.testhunk(old, self.lines, l) == 0:
701 newlines = h.new(fuzzlen, toponly)
701 newlines = h.new(fuzzlen, toponly)
702 self.lines[l : l + len(old)] = newlines
702 self.lines[l : l + len(old)] = newlines
703 self.offset += len(newlines) - len(old)
703 self.offset += len(newlines) - len(old)
704 self.skew = l - orig_start
704 self.skew = l - orig_start
705 self.dirty = True
705 self.dirty = True
706 offset = l - orig_start - fuzzlen
706 offset = l - orig_start - fuzzlen
707 if fuzzlen:
707 if fuzzlen:
708 msg = _("Hunk #%d succeeded at %d "
708 msg = _("Hunk #%d succeeded at %d "
709 "with fuzz %d "
709 "with fuzz %d "
710 "(offset %d lines).\n")
710 "(offset %d lines).\n")
711 self.printfile(True)
711 self.printfile(True)
712 self.ui.warn(msg %
712 self.ui.warn(msg %
713 (h.number, l + 1, fuzzlen, offset))
713 (h.number, l + 1, fuzzlen, offset))
714 else:
714 else:
715 msg = _("Hunk #%d succeeded at %d "
715 msg = _("Hunk #%d succeeded at %d "
716 "(offset %d lines).\n")
716 "(offset %d lines).\n")
717 self.ui.note(msg % (h.number, l + 1, offset))
717 self.ui.note(msg % (h.number, l + 1, offset))
718 return fuzzlen
718 return fuzzlen
719 self.printfile(True)
719 self.printfile(True)
720 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))
721 self.rej.append(horig)
721 self.rej.append(horig)
722 return -1
722 return -1
723
723
724 def close(self):
724 def close(self):
725 if self.dirty:
725 if self.dirty:
726 self.writelines(self.fname, self.lines, self.mode)
726 self.writelines(self.fname, self.lines, self.mode)
727 self.write_rej()
727 self.write_rej()
728 return len(self.rej)
728 return len(self.rej)
729
729
730 class hunk(object):
730 class hunk(object):
731 def __init__(self, desc, num, lr, context, create=False, remove=False):
731 def __init__(self, desc, num, lr, context, create=False, remove=False):
732 self.number = num
732 self.number = num
733 self.desc = desc
733 self.desc = desc
734 self.hunk = [desc]
734 self.hunk = [desc]
735 self.a = []
735 self.a = []
736 self.b = []
736 self.b = []
737 self.starta = self.lena = None
737 self.starta = self.lena = None
738 self.startb = self.lenb = None
738 self.startb = self.lenb = None
739 if lr is not None:
739 if lr is not None:
740 if context:
740 if context:
741 self.read_context_hunk(lr)
741 self.read_context_hunk(lr)
742 else:
742 else:
743 self.read_unified_hunk(lr)
743 self.read_unified_hunk(lr)
744 self.create = create
744 self.create = create
745 self.remove = remove and not create
745 self.remove = remove and not create
746
746
747 def getnormalized(self):
747 def getnormalized(self):
748 """Return a copy with line endings normalized to LF."""
748 """Return a copy with line endings normalized to LF."""
749
749
750 def normalize(lines):
750 def normalize(lines):
751 nlines = []
751 nlines = []
752 for line in lines:
752 for line in lines:
753 if line.endswith('\r\n'):
753 if line.endswith('\r\n'):
754 line = line[:-2] + '\n'
754 line = line[:-2] + '\n'
755 nlines.append(line)
755 nlines.append(line)
756 return nlines
756 return nlines
757
757
758 # Dummy object, it is rebuilt manually
758 # Dummy object, it is rebuilt manually
759 nh = hunk(self.desc, self.number, None, None, False, False)
759 nh = hunk(self.desc, self.number, None, None, False, False)
760 nh.number = self.number
760 nh.number = self.number
761 nh.desc = self.desc
761 nh.desc = self.desc
762 nh.hunk = self.hunk
762 nh.hunk = self.hunk
763 nh.a = normalize(self.a)
763 nh.a = normalize(self.a)
764 nh.b = normalize(self.b)
764 nh.b = normalize(self.b)
765 nh.starta = self.starta
765 nh.starta = self.starta
766 nh.startb = self.startb
766 nh.startb = self.startb
767 nh.lena = self.lena
767 nh.lena = self.lena
768 nh.lenb = self.lenb
768 nh.lenb = self.lenb
769 nh.create = self.create
769 nh.create = self.create
770 nh.remove = self.remove
770 nh.remove = self.remove
771 return nh
771 return nh
772
772
773 def read_unified_hunk(self, lr):
773 def read_unified_hunk(self, lr):
774 m = unidesc.match(self.desc)
774 m = unidesc.match(self.desc)
775 if not m:
775 if not m:
776 raise PatchError(_("bad hunk #%d") % self.number)
776 raise PatchError(_("bad hunk #%d") % self.number)
777 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()
778 if self.lena is None:
778 if self.lena is None:
779 self.lena = 1
779 self.lena = 1
780 else:
780 else:
781 self.lena = int(self.lena)
781 self.lena = int(self.lena)
782 if self.lenb is None:
782 if self.lenb is None:
783 self.lenb = 1
783 self.lenb = 1
784 else:
784 else:
785 self.lenb = int(self.lenb)
785 self.lenb = int(self.lenb)
786 self.starta = int(self.starta)
786 self.starta = int(self.starta)
787 self.startb = int(self.startb)
787 self.startb = int(self.startb)
788 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)
789 # 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
790 # be zero length. Lets try to fix it up.
790 # be zero length. Lets try to fix it up.
791 while len(self.hunk[-1]) == 0:
791 while len(self.hunk[-1]) == 0:
792 del self.hunk[-1]
792 del self.hunk[-1]
793 del self.a[-1]
793 del self.a[-1]
794 del self.b[-1]
794 del self.b[-1]
795 self.lena -= 1
795 self.lena -= 1
796 self.lenb -= 1
796 self.lenb -= 1
797 self._fixnewline(lr)
797 self._fixnewline(lr)
798
798
799 def read_context_hunk(self, lr):
799 def read_context_hunk(self, lr):
800 self.desc = lr.readline()
800 self.desc = lr.readline()
801 m = contextdesc.match(self.desc)
801 m = contextdesc.match(self.desc)
802 if not m:
802 if not m:
803 raise PatchError(_("bad hunk #%d") % self.number)
803 raise PatchError(_("bad hunk #%d") % self.number)
804 foo, self.starta, foo2, aend, foo3 = m.groups()
804 foo, self.starta, foo2, aend, foo3 = m.groups()
805 self.starta = int(self.starta)
805 self.starta = int(self.starta)
806 if aend is None:
806 if aend is None:
807 aend = self.starta
807 aend = self.starta
808 self.lena = int(aend) - self.starta
808 self.lena = int(aend) - self.starta
809 if self.starta:
809 if self.starta:
810 self.lena += 1
810 self.lena += 1
811 for x in xrange(self.lena):
811 for x in xrange(self.lena):
812 l = lr.readline()
812 l = lr.readline()
813 if l.startswith('---'):
813 if l.startswith('---'):
814 # lines addition, old block is empty
814 # lines addition, old block is empty
815 lr.push(l)
815 lr.push(l)
816 break
816 break
817 s = l[2:]
817 s = l[2:]
818 if l.startswith('- ') or l.startswith('! '):
818 if l.startswith('- ') or l.startswith('! '):
819 u = '-' + s
819 u = '-' + s
820 elif l.startswith(' '):
820 elif l.startswith(' '):
821 u = ' ' + s
821 u = ' ' + s
822 else:
822 else:
823 raise PatchError(_("bad hunk #%d old text line %d") %
823 raise PatchError(_("bad hunk #%d old text line %d") %
824 (self.number, x))
824 (self.number, x))
825 self.a.append(u)
825 self.a.append(u)
826 self.hunk.append(u)
826 self.hunk.append(u)
827
827
828 l = lr.readline()
828 l = lr.readline()
829 if l.startswith('\ '):
829 if l.startswith('\ '):
830 s = self.a[-1][:-1]
830 s = self.a[-1][:-1]
831 self.a[-1] = s
831 self.a[-1] = s
832 self.hunk[-1] = s
832 self.hunk[-1] = s
833 l = lr.readline()
833 l = lr.readline()
834 m = contextdesc.match(l)
834 m = contextdesc.match(l)
835 if not m:
835 if not m:
836 raise PatchError(_("bad hunk #%d") % self.number)
836 raise PatchError(_("bad hunk #%d") % self.number)
837 foo, self.startb, foo2, bend, foo3 = m.groups()
837 foo, self.startb, foo2, bend, foo3 = m.groups()
838 self.startb = int(self.startb)
838 self.startb = int(self.startb)
839 if bend is None:
839 if bend is None:
840 bend = self.startb
840 bend = self.startb
841 self.lenb = int(bend) - self.startb
841 self.lenb = int(bend) - self.startb
842 if self.startb:
842 if self.startb:
843 self.lenb += 1
843 self.lenb += 1
844 hunki = 1
844 hunki = 1
845 for x in xrange(self.lenb):
845 for x in xrange(self.lenb):
846 l = lr.readline()
846 l = lr.readline()
847 if l.startswith('\ '):
847 if l.startswith('\ '):
848 # 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.
849 # 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
850 # guess there are diff(1) out there which behave differently.
850 # guess there are diff(1) out there which behave differently.
851 s = self.b[-1][:-1]
851 s = self.b[-1][:-1]
852 self.b[-1] = s
852 self.b[-1] = s
853 self.hunk[hunki - 1] = s
853 self.hunk[hunki - 1] = s
854 continue
854 continue
855 if not l:
855 if not l:
856 # line deletions, new block is empty and we hit EOF
856 # line deletions, new block is empty and we hit EOF
857 lr.push(l)
857 lr.push(l)
858 break
858 break
859 s = l[2:]
859 s = l[2:]
860 if l.startswith('+ ') or l.startswith('! '):
860 if l.startswith('+ ') or l.startswith('! '):
861 u = '+' + s
861 u = '+' + s
862 elif l.startswith(' '):
862 elif l.startswith(' '):
863 u = ' ' + s
863 u = ' ' + s
864 elif len(self.b) == 0:
864 elif len(self.b) == 0:
865 # line deletions, new block is empty
865 # line deletions, new block is empty
866 lr.push(l)
866 lr.push(l)
867 break
867 break
868 else:
868 else:
869 raise PatchError(_("bad hunk #%d old text line %d") %
869 raise PatchError(_("bad hunk #%d old text line %d") %
870 (self.number, x))
870 (self.number, x))
871 self.b.append(s)
871 self.b.append(s)
872 while True:
872 while True:
873 if hunki >= len(self.hunk):
873 if hunki >= len(self.hunk):
874 h = ""
874 h = ""
875 else:
875 else:
876 h = self.hunk[hunki]
876 h = self.hunk[hunki]
877 hunki += 1
877 hunki += 1
878 if h == u:
878 if h == u:
879 break
879 break
880 elif h.startswith('-'):
880 elif h.startswith('-'):
881 continue
881 continue
882 else:
882 else:
883 self.hunk.insert(hunki - 1, u)
883 self.hunk.insert(hunki - 1, u)
884 break
884 break
885
885
886 if not self.a:
886 if not self.a:
887 # this happens when lines were only added to the hunk
887 # this happens when lines were only added to the hunk
888 for x in self.hunk:
888 for x in self.hunk:
889 if x.startswith('-') or x.startswith(' '):
889 if x.startswith('-') or x.startswith(' '):
890 self.a.append(x)
890 self.a.append(x)
891 if not self.b:
891 if not self.b:
892 # this happens when lines were only deleted from the hunk
892 # this happens when lines were only deleted from the hunk
893 for x in self.hunk:
893 for x in self.hunk:
894 if x.startswith('+') or x.startswith(' '):
894 if x.startswith('+') or x.startswith(' '):
895 self.b.append(x[1:])
895 self.b.append(x[1:])
896 # @@ -start,len +start,len @@
896 # @@ -start,len +start,len @@
897 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
897 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
898 self.startb, self.lenb)
898 self.startb, self.lenb)
899 self.hunk[0] = self.desc
899 self.hunk[0] = self.desc
900 self._fixnewline(lr)
900 self._fixnewline(lr)
901
901
902 def _fixnewline(self, lr):
902 def _fixnewline(self, lr):
903 l = lr.readline()
903 l = lr.readline()
904 if l.startswith('\ '):
904 if l.startswith('\ '):
905 diffhelpers.fix_newline(self.hunk, self.a, self.b)
905 diffhelpers.fix_newline(self.hunk, self.a, self.b)
906 else:
906 else:
907 lr.push(l)
907 lr.push(l)
908
908
909 def complete(self):
909 def complete(self):
910 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
911
911
912 def createfile(self):
912 def createfile(self):
913 return self.starta == 0 and self.lena == 0 and self.create
913 return self.starta == 0 and self.lena == 0 and self.create
914
914
915 def rmfile(self):
915 def rmfile(self):
916 return self.startb == 0 and self.lenb == 0 and self.remove
916 return self.startb == 0 and self.lenb == 0 and self.remove
917
917
918 def fuzzit(self, l, fuzz, toponly):
918 def fuzzit(self, l, fuzz, toponly):
919 # 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
920 # 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
921 # returns a new shortened list of lines.
921 # returns a new shortened list of lines.
922 fuzz = min(fuzz, len(l)-1)
922 fuzz = min(fuzz, len(l)-1)
923 if fuzz:
923 if fuzz:
924 top = 0
924 top = 0
925 bot = 0
925 bot = 0
926 hlen = len(self.hunk)
926 hlen = len(self.hunk)
927 for x in xrange(hlen - 1):
927 for x in xrange(hlen - 1):
928 # the hunk starts with the @@ line, so use x+1
928 # the hunk starts with the @@ line, so use x+1
929 if self.hunk[x + 1][0] == ' ':
929 if self.hunk[x + 1][0] == ' ':
930 top += 1
930 top += 1
931 else:
931 else:
932 break
932 break
933 if not toponly:
933 if not toponly:
934 for x in xrange(hlen - 1):
934 for x in xrange(hlen - 1):
935 if self.hunk[hlen - bot - 1][0] == ' ':
935 if self.hunk[hlen - bot - 1][0] == ' ':
936 bot += 1
936 bot += 1
937 else:
937 else:
938 break
938 break
939
939
940 # top and bot now count context in the hunk
940 # top and bot now count context in the hunk
941 # adjust them if either one is short
941 # adjust them if either one is short
942 context = max(top, bot, 3)
942 context = max(top, bot, 3)
943 if bot < context:
943 if bot < context:
944 bot = max(0, fuzz - (context - bot))
944 bot = max(0, fuzz - (context - bot))
945 else:
945 else:
946 bot = min(fuzz, bot)
946 bot = min(fuzz, bot)
947 if top < context:
947 if top < context:
948 top = max(0, fuzz - (context - top))
948 top = max(0, fuzz - (context - top))
949 else:
949 else:
950 top = min(fuzz, top)
950 top = min(fuzz, top)
951
951
952 return l[top:len(l)-bot]
952 return l[top:len(l)-bot]
953 return l
953 return l
954
954
955 def old(self, fuzz=0, toponly=False):
955 def old(self, fuzz=0, toponly=False):
956 return self.fuzzit(self.a, fuzz, toponly)
956 return self.fuzzit(self.a, fuzz, toponly)
957
957
958 def new(self, fuzz=0, toponly=False):
958 def new(self, fuzz=0, toponly=False):
959 return self.fuzzit(self.b, fuzz, toponly)
959 return self.fuzzit(self.b, fuzz, toponly)
960
960
961 class binhunk:
961 class binhunk:
962 'A binary patch file. Only understands literals so far.'
962 'A binary patch file. Only understands literals so far.'
963 def __init__(self, gitpatch, lr):
963 def __init__(self, gitpatch, lr):
964 self.gitpatch = gitpatch
964 self.gitpatch = gitpatch
965 self.text = None
965 self.text = None
966 self.hunk = ['GIT binary patch\n']
966 self.hunk = ['GIT binary patch\n']
967 self._read(lr)
967 self._read(lr)
968
968
969 def createfile(self):
969 def createfile(self):
970 return self.gitpatch.op == 'ADD'
970 return self.gitpatch.op == 'ADD'
971
971
972 def rmfile(self):
972 def rmfile(self):
973 return self.gitpatch.op == 'DELETE'
973 return self.gitpatch.op == 'DELETE'
974
974
975 def complete(self):
975 def complete(self):
976 return self.text is not None
976 return self.text is not None
977
977
978 def new(self):
978 def new(self):
979 return [self.text]
979 return [self.text]
980
980
981 def _read(self, lr):
981 def _read(self, lr):
982 line = lr.readline()
982 line = lr.readline()
983 self.hunk.append(line)
983 self.hunk.append(line)
984 while line and not line.startswith('literal '):
984 while line and not line.startswith('literal '):
985 line = lr.readline()
985 line = lr.readline()
986 self.hunk.append(line)
986 self.hunk.append(line)
987 if not line:
987 if not line:
988 raise PatchError(_('could not extract binary patch'))
988 raise PatchError(_('could not extract binary patch'))
989 size = int(line[8:].rstrip())
989 size = int(line[8:].rstrip())
990 dec = []
990 dec = []
991 line = lr.readline()
991 line = lr.readline()
992 self.hunk.append(line)
992 self.hunk.append(line)
993 while len(line) > 1:
993 while len(line) > 1:
994 l = line[0]
994 l = line[0]
995 if l <= 'Z' and l >= 'A':
995 if l <= 'Z' and l >= 'A':
996 l = ord(l) - ord('A') + 1
996 l = ord(l) - ord('A') + 1
997 else:
997 else:
998 l = ord(l) - ord('a') + 27
998 l = ord(l) - ord('a') + 27
999 dec.append(base85.b85decode(line[1:-1])[:l])
999 dec.append(base85.b85decode(line[1:-1])[:l])
1000 line = lr.readline()
1000 line = lr.readline()
1001 self.hunk.append(line)
1001 self.hunk.append(line)
1002 text = zlib.decompress(''.join(dec))
1002 text = zlib.decompress(''.join(dec))
1003 if len(text) != size:
1003 if len(text) != size:
1004 raise PatchError(_('binary patch is %d bytes, not %d') %
1004 raise PatchError(_('binary patch is %d bytes, not %d') %
1005 len(text), size)
1005 len(text), size)
1006 self.text = text
1006 self.text = text
1007
1007
1008 def parsefilename(str):
1008 def parsefilename(str):
1009 # --- filename \t|space stuff
1009 # --- filename \t|space stuff
1010 s = str[4:].rstrip('\r\n')
1010 s = str[4:].rstrip('\r\n')
1011 i = s.find('\t')
1011 i = s.find('\t')
1012 if i < 0:
1012 if i < 0:
1013 i = s.find(' ')
1013 i = s.find(' ')
1014 if i < 0:
1014 if i < 0:
1015 return s
1015 return s
1016 return s[:i]
1016 return s[:i]
1017
1017
1018 def pathstrip(path, strip):
1018 def pathstrip(path, strip):
1019 pathlen = len(path)
1019 pathlen = len(path)
1020 i = 0
1020 i = 0
1021 if strip == 0:
1021 if strip == 0:
1022 return '', path.rstrip()
1022 return '', path.rstrip()
1023 count = strip
1023 count = strip
1024 while count > 0:
1024 while count > 0:
1025 i = path.find('/', i)
1025 i = path.find('/', i)
1026 if i == -1:
1026 if i == -1:
1027 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1027 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1028 (count, strip, path))
1028 (count, strip, path))
1029 i += 1
1029 i += 1
1030 # consume '//' in the path
1030 # consume '//' in the path
1031 while i < pathlen - 1 and path[i] == '/':
1031 while i < pathlen - 1 and path[i] == '/':
1032 i += 1
1032 i += 1
1033 count -= 1
1033 count -= 1
1034 return path[:i].lstrip(), path[i:].rstrip()
1034 return path[:i].lstrip(), path[i:].rstrip()
1035
1035
1036 def selectfile(backend, afile_orig, bfile_orig, hunk, strip):
1036 def selectfile(backend, afile_orig, bfile_orig, hunk, strip):
1037 nulla = afile_orig == "/dev/null"
1037 nulla = afile_orig == "/dev/null"
1038 nullb = bfile_orig == "/dev/null"
1038 nullb = bfile_orig == "/dev/null"
1039 abase, afile = pathstrip(afile_orig, strip)
1039 abase, afile = pathstrip(afile_orig, strip)
1040 gooda = not nulla and backend.exists(afile)
1040 gooda = not nulla and backend.exists(afile)
1041 bbase, bfile = pathstrip(bfile_orig, strip)
1041 bbase, bfile = pathstrip(bfile_orig, strip)
1042 if afile == bfile:
1042 if afile == bfile:
1043 goodb = gooda
1043 goodb = gooda
1044 else:
1044 else:
1045 goodb = not nullb and backend.exists(bfile)
1045 goodb = not nullb and backend.exists(bfile)
1046 createfunc = hunk.createfile
1046 createfunc = hunk.createfile
1047 missing = not goodb and not gooda and not createfunc()
1047 missing = not goodb and not gooda and not createfunc()
1048
1048
1049 # some diff programs apparently produce patches where the afile is
1049 # some diff programs apparently produce patches where the afile is
1050 # not /dev/null, but afile starts with bfile
1050 # not /dev/null, but afile starts with bfile
1051 abasedir = afile[:afile.rfind('/') + 1]
1051 abasedir = afile[:afile.rfind('/') + 1]
1052 bbasedir = bfile[:bfile.rfind('/') + 1]
1052 bbasedir = bfile[:bfile.rfind('/') + 1]
1053 if missing and abasedir == bbasedir and afile.startswith(bfile):
1053 if missing and abasedir == bbasedir and afile.startswith(bfile):
1054 # this isn't very pretty
1054 # this isn't very pretty
1055 hunk.create = True
1055 hunk.create = True
1056 if createfunc():
1056 if createfunc():
1057 missing = False
1057 missing = False
1058 else:
1058 else:
1059 hunk.create = False
1059 hunk.create = False
1060
1060
1061 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1061 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1062 # diff is between a file and its backup. In this case, the original
1062 # diff is between a file and its backup. In this case, the original
1063 # file should be patched (see original mpatch code).
1063 # file should be patched (see original mpatch code).
1064 isbackup = (abase == bbase and bfile.startswith(afile))
1064 isbackup = (abase == bbase and bfile.startswith(afile))
1065 fname = None
1065 fname = None
1066 if not missing:
1066 if not missing:
1067 if gooda and goodb:
1067 if gooda and goodb:
1068 fname = isbackup and afile or bfile
1068 fname = isbackup and afile or bfile
1069 elif gooda:
1069 elif gooda:
1070 fname = afile
1070 fname = afile
1071
1071
1072 if not fname:
1072 if not fname:
1073 if not nullb:
1073 if not nullb:
1074 fname = isbackup and afile or bfile
1074 fname = isbackup and afile or bfile
1075 elif not nulla:
1075 elif not nulla:
1076 fname = afile
1076 fname = afile
1077 else:
1077 else:
1078 raise PatchError(_("undefined source and destination files"))
1078 raise PatchError(_("undefined source and destination files"))
1079
1079
1080 return fname, missing
1080 return fname, missing
1081
1081
1082 def scangitpatch(lr, firstline):
1082 def scangitpatch(lr, firstline):
1083 """
1083 """
1084 Git patches can emit:
1084 Git patches can emit:
1085 - rename a to b
1085 - rename a to b
1086 - change b
1086 - change b
1087 - copy a to c
1087 - copy a to c
1088 - change c
1088 - change c
1089
1089
1090 We cannot apply this sequence as-is, the renamed 'a' could not be
1090 We cannot apply this sequence as-is, the renamed 'a' could not be
1091 found for it would have been renamed already. And we cannot copy
1091 found for it would have been renamed already. And we cannot copy
1092 from 'b' instead because 'b' would have been changed already. So
1092 from 'b' instead because 'b' would have been changed already. So
1093 we scan the git patch for copy and rename commands so we can
1093 we scan the git patch for copy and rename commands so we can
1094 perform the copies ahead of time.
1094 perform the copies ahead of time.
1095 """
1095 """
1096 pos = 0
1096 pos = 0
1097 try:
1097 try:
1098 pos = lr.fp.tell()
1098 pos = lr.fp.tell()
1099 fp = lr.fp
1099 fp = lr.fp
1100 except IOError:
1100 except IOError:
1101 fp = cStringIO.StringIO(lr.fp.read())
1101 fp = cStringIO.StringIO(lr.fp.read())
1102 gitlr = linereader(fp, lr.textmode)
1102 gitlr = linereader(fp, lr.textmode)
1103 gitlr.push(firstline)
1103 gitlr.push(firstline)
1104 gitpatches = readgitpatch(gitlr)
1104 gitpatches = readgitpatch(gitlr)
1105 fp.seek(pos)
1105 fp.seek(pos)
1106 return gitpatches
1106 return gitpatches
1107
1107
1108 def iterhunks(fp):
1108 def iterhunks(fp):
1109 """Read a patch and yield the following events:
1109 """Read a patch and yield the following events:
1110 - ("file", afile, bfile, firsthunk): select a new target file.
1110 - ("file", afile, bfile, firsthunk): select a new target file.
1111 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1111 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1112 "file" event.
1112 "file" event.
1113 - ("git", gitchanges): current diff is in git format, gitchanges
1113 - ("git", gitchanges): current diff is in git format, gitchanges
1114 maps filenames to gitpatch records. Unique event.
1114 maps filenames to gitpatch records. Unique event.
1115 """
1115 """
1116 changed = {}
1116 changed = {}
1117 afile = ""
1117 afile = ""
1118 bfile = ""
1118 bfile = ""
1119 state = None
1119 state = None
1120 hunknum = 0
1120 hunknum = 0
1121 emitfile = newfile = False
1121 emitfile = newfile = False
1122 git = False
1122 git = False
1123
1123
1124 # our states
1124 # our states
1125 BFILE = 1
1125 BFILE = 1
1126 context = None
1126 context = None
1127 lr = linereader(fp)
1127 lr = linereader(fp)
1128
1128
1129 while True:
1129 while True:
1130 x = lr.readline()
1130 x = lr.readline()
1131 if not x:
1131 if not x:
1132 break
1132 break
1133 if state == BFILE and (
1133 if state == BFILE and (
1134 (not context and x[0] == '@')
1134 (not context and x[0] == '@')
1135 or (context is not False and x.startswith('***************'))
1135 or (context is not False and x.startswith('***************'))
1136 or x.startswith('GIT binary patch')):
1136 or x.startswith('GIT binary patch')):
1137 gp = changed.get(bfile)
1137 gp = changed.get(bfile)
1138 if x.startswith('GIT binary patch'):
1138 if x.startswith('GIT binary patch'):
1139 h = binhunk(gp, lr)
1139 h = binhunk(gp, lr)
1140 afile = 'a/' + afile
1140 afile = 'a/' + afile
1141 bfile = 'b/' + bfile
1141 bfile = 'b/' + bfile
1142 else:
1142 else:
1143 if context is None and x.startswith('***************'):
1143 if context is None and x.startswith('***************'):
1144 context = True
1144 context = True
1145 create = afile == '/dev/null' or gp and gp.op == 'ADD'
1145 create = afile == '/dev/null' or gp and gp.op == 'ADD'
1146 remove = bfile == '/dev/null' or gp and gp.op == 'DELETE'
1146 remove = bfile == '/dev/null' or gp and gp.op == 'DELETE'
1147 h = hunk(x, hunknum + 1, lr, context, create, remove)
1147 h = hunk(x, hunknum + 1, lr, context, create, remove)
1148 hunknum += 1
1148 hunknum += 1
1149 if emitfile:
1149 if emitfile:
1150 emitfile = False
1150 emitfile = False
1151 yield 'file', (afile, bfile, h, gp and gp.mode or None)
1151 yield 'file', (afile, bfile, h, gp and gp.mode or None)
1152 yield 'hunk', h
1152 yield 'hunk', h
1153 elif x.startswith('diff --git'):
1153 elif x.startswith('diff --git'):
1154 # check for git diff, scanning the whole patch file if needed
1154 # check for git diff, scanning the whole patch file if needed
1155 m = gitre.match(x)
1155 m = gitre.match(x)
1156 if m:
1156 if m:
1157 afile, bfile = m.group(1, 2)
1157 afile, bfile = m.group(1, 2)
1158 if not git:
1158 if not git:
1159 git = True
1159 git = True
1160 gitpatches = scangitpatch(lr, x)
1160 gitpatches = scangitpatch(lr, x)
1161 yield 'git', gitpatches
1162 for gp in gitpatches:
1161 for gp in gitpatches:
1163 changed[gp.path] = gp
1162 changed[gp.path] = gp
1164 # else error?
1163 yield 'git', gitpatches
1165 # copy/rename + modify should modify target, not source
1164 # copy/rename + modify should modify target, not source
1166 gp = changed.get(bfile)
1165 gp = changed[bfile]
1167 if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
1166 if gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD') or gp.mode:
1168 or gp.mode):
1169 afile = bfile
1167 afile = bfile
1170 newfile = True
1168 newfile = True
1171 elif x.startswith('---'):
1169 elif x.startswith('---'):
1172 # check for a unified diff
1170 # check for a unified diff
1173 l2 = lr.readline()
1171 l2 = lr.readline()
1174 if not l2.startswith('+++'):
1172 if not l2.startswith('+++'):
1175 lr.push(l2)
1173 lr.push(l2)
1176 continue
1174 continue
1177 newfile = True
1175 newfile = True
1178 context = False
1176 context = False
1179 afile = parsefilename(x)
1177 afile = parsefilename(x)
1180 bfile = parsefilename(l2)
1178 bfile = parsefilename(l2)
1181 elif x.startswith('***'):
1179 elif x.startswith('***'):
1182 # check for a context diff
1180 # check for a context diff
1183 l2 = lr.readline()
1181 l2 = lr.readline()
1184 if not l2.startswith('---'):
1182 if not l2.startswith('---'):
1185 lr.push(l2)
1183 lr.push(l2)
1186 continue
1184 continue
1187 l3 = lr.readline()
1185 l3 = lr.readline()
1188 lr.push(l3)
1186 lr.push(l3)
1189 if not l3.startswith("***************"):
1187 if not l3.startswith("***************"):
1190 lr.push(l2)
1188 lr.push(l2)
1191 continue
1189 continue
1192 newfile = True
1190 newfile = True
1193 context = True
1191 context = True
1194 afile = parsefilename(x)
1192 afile = parsefilename(x)
1195 bfile = parsefilename(l2)
1193 bfile = parsefilename(l2)
1196
1194
1197 if newfile:
1195 if newfile:
1198 newfile = False
1196 newfile = False
1199 emitfile = True
1197 emitfile = True
1200 state = BFILE
1198 state = BFILE
1201 hunknum = 0
1199 hunknum = 0
1202
1200
1203 def applydiff(ui, fp, changed, backend, strip=1, eolmode='strict'):
1201 def applydiff(ui, fp, changed, backend, strip=1, eolmode='strict'):
1204 """Reads a patch from fp and tries to apply it.
1202 """Reads a patch from fp and tries to apply it.
1205
1203
1206 The dict 'changed' is filled in with all of the filenames changed
1204 The dict 'changed' is filled in with all of the filenames changed
1207 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1205 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1208 found and 1 if there was any fuzz.
1206 found and 1 if there was any fuzz.
1209
1207
1210 If 'eolmode' is 'strict', the patch content and patched file are
1208 If 'eolmode' is 'strict', the patch content and patched file are
1211 read in binary mode. Otherwise, line endings are ignored when
1209 read in binary mode. Otherwise, line endings are ignored when
1212 patching then normalized according to 'eolmode'.
1210 patching then normalized according to 'eolmode'.
1213 """
1211 """
1214 return _applydiff(ui, fp, patchfile, backend, changed, strip=strip,
1212 return _applydiff(ui, fp, patchfile, backend, changed, strip=strip,
1215 eolmode=eolmode)
1213 eolmode=eolmode)
1216
1214
1217 def _applydiff(ui, fp, patcher, backend, changed, strip=1, eolmode='strict'):
1215 def _applydiff(ui, fp, patcher, backend, changed, strip=1, eolmode='strict'):
1218 rejects = 0
1216 rejects = 0
1219 err = 0
1217 err = 0
1220 current_file = None
1218 current_file = None
1221
1219
1222 for state, values in iterhunks(fp):
1220 for state, values in iterhunks(fp):
1223 if state == 'hunk':
1221 if state == 'hunk':
1224 if not current_file:
1222 if not current_file:
1225 continue
1223 continue
1226 ret = current_file.apply(values)
1224 ret = current_file.apply(values)
1227 if ret >= 0:
1225 if ret >= 0:
1228 changed.setdefault(current_file.fname, None)
1226 changed.setdefault(current_file.fname, None)
1229 if ret > 0:
1227 if ret > 0:
1230 err = 1
1228 err = 1
1231 elif state == 'file':
1229 elif state == 'file':
1232 if current_file:
1230 if current_file:
1233 rejects += current_file.close()
1231 rejects += current_file.close()
1234 afile, bfile, first_hunk, mode = values
1232 afile, bfile, first_hunk, mode = values
1235 try:
1233 try:
1236 current_file, missing = selectfile(backend, afile, bfile,
1234 current_file, missing = selectfile(backend, afile, bfile,
1237 first_hunk, strip)
1235 first_hunk, strip)
1238 current_file = patcher(ui, current_file, backend, mode,
1236 current_file = patcher(ui, current_file, backend, mode,
1239 missing=missing, eolmode=eolmode)
1237 missing=missing, eolmode=eolmode)
1240 except PatchError, inst:
1238 except PatchError, inst:
1241 ui.warn(str(inst) + '\n')
1239 ui.warn(str(inst) + '\n')
1242 current_file = None
1240 current_file = None
1243 rejects += 1
1241 rejects += 1
1244 continue
1242 continue
1245 elif state == 'git':
1243 elif state == 'git':
1246 for gp in values:
1244 for gp in values:
1247 gp.path = pathstrip(gp.path, strip - 1)[1]
1245 gp.path = pathstrip(gp.path, strip - 1)[1]
1248 if gp.oldpath:
1246 if gp.oldpath:
1249 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1247 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1250 if gp.op in ('COPY', 'RENAME'):
1248 if gp.op in ('COPY', 'RENAME'):
1251 backend.copy(gp.oldpath, gp.path)
1249 backend.copy(gp.oldpath, gp.path)
1252 changed[gp.path] = gp
1250 changed[gp.path] = gp
1253 else:
1251 else:
1254 raise util.Abort(_('unsupported parser state: %s') % state)
1252 raise util.Abort(_('unsupported parser state: %s') % state)
1255
1253
1256 if current_file:
1254 if current_file:
1257 rejects += current_file.close()
1255 rejects += current_file.close()
1258
1256
1259 # Handle mode changes without hunk
1257 # Handle mode changes without hunk
1260 removed = set()
1258 removed = set()
1261 for gp in changed.itervalues():
1259 for gp in changed.itervalues():
1262 if not gp:
1260 if not gp:
1263 continue
1261 continue
1264 if gp.op == 'DELETE':
1262 if gp.op == 'DELETE':
1265 removed.add(gp.path)
1263 removed.add(gp.path)
1266 continue
1264 continue
1267 if gp.op == 'RENAME':
1265 if gp.op == 'RENAME':
1268 removed.add(gp.oldpath)
1266 removed.add(gp.oldpath)
1269 if gp.mode:
1267 if gp.mode:
1270 if gp.op == 'ADD' and not backend.exists(gp.path):
1268 if gp.op == 'ADD' and not backend.exists(gp.path):
1271 # Added files without content have no hunk and must be created
1269 # Added files without content have no hunk and must be created
1272 backend.writelines(gp.path, [], gp.mode)
1270 backend.writelines(gp.path, [], gp.mode)
1273 else:
1271 else:
1274 backend.setmode(gp.path, gp.mode[0], gp.mode[1])
1272 backend.setmode(gp.path, gp.mode[0], gp.mode[1])
1275 for path in sorted(removed):
1273 for path in sorted(removed):
1276 backend.unlink(path)
1274 backend.unlink(path)
1277
1275
1278 if rejects:
1276 if rejects:
1279 return -1
1277 return -1
1280 return err
1278 return err
1281
1279
1282 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1280 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1283 similarity):
1281 similarity):
1284 """use <patcher> to apply <patchname> to the working directory.
1282 """use <patcher> to apply <patchname> to the working directory.
1285 returns whether patch was applied with fuzz factor."""
1283 returns whether patch was applied with fuzz factor."""
1286
1284
1287 fuzz = False
1285 fuzz = False
1288 args = []
1286 args = []
1289 cwd = repo.root
1287 cwd = repo.root
1290 if cwd:
1288 if cwd:
1291 args.append('-d %s' % util.shellquote(cwd))
1289 args.append('-d %s' % util.shellquote(cwd))
1292 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1290 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1293 util.shellquote(patchname)))
1291 util.shellquote(patchname)))
1294 try:
1292 try:
1295 for line in fp:
1293 for line in fp:
1296 line = line.rstrip()
1294 line = line.rstrip()
1297 ui.note(line + '\n')
1295 ui.note(line + '\n')
1298 if line.startswith('patching file '):
1296 if line.startswith('patching file '):
1299 pf = util.parsepatchoutput(line)
1297 pf = util.parsepatchoutput(line)
1300 printed_file = False
1298 printed_file = False
1301 files.setdefault(pf, None)
1299 files.setdefault(pf, None)
1302 elif line.find('with fuzz') >= 0:
1300 elif line.find('with fuzz') >= 0:
1303 fuzz = True
1301 fuzz = True
1304 if not printed_file:
1302 if not printed_file:
1305 ui.warn(pf + '\n')
1303 ui.warn(pf + '\n')
1306 printed_file = True
1304 printed_file = True
1307 ui.warn(line + '\n')
1305 ui.warn(line + '\n')
1308 elif line.find('saving rejects to file') >= 0:
1306 elif line.find('saving rejects to file') >= 0:
1309 ui.warn(line + '\n')
1307 ui.warn(line + '\n')
1310 elif line.find('FAILED') >= 0:
1308 elif line.find('FAILED') >= 0:
1311 if not printed_file:
1309 if not printed_file:
1312 ui.warn(pf + '\n')
1310 ui.warn(pf + '\n')
1313 printed_file = True
1311 printed_file = True
1314 ui.warn(line + '\n')
1312 ui.warn(line + '\n')
1315 finally:
1313 finally:
1316 if files:
1314 if files:
1317 cfiles = list(files)
1315 cfiles = list(files)
1318 cwd = repo.getcwd()
1316 cwd = repo.getcwd()
1319 if cwd:
1317 if cwd:
1320 cfiles = [util.pathto(repo.root, cwd, f)
1318 cfiles = [util.pathto(repo.root, cwd, f)
1321 for f in cfile]
1319 for f in cfile]
1322 scmutil.addremove(repo, cfiles, similarity=similarity)
1320 scmutil.addremove(repo, cfiles, similarity=similarity)
1323 code = fp.close()
1321 code = fp.close()
1324 if code:
1322 if code:
1325 raise PatchError(_("patch command failed: %s") %
1323 raise PatchError(_("patch command failed: %s") %
1326 util.explainexit(code)[0])
1324 util.explainexit(code)[0])
1327 return fuzz
1325 return fuzz
1328
1326
1329 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1327 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1330 similarity=0):
1328 similarity=0):
1331 """use builtin patch to apply <patchobj> to the working directory.
1329 """use builtin patch to apply <patchobj> to the working directory.
1332 returns whether patch was applied with fuzz factor."""
1330 returns whether patch was applied with fuzz factor."""
1333
1331
1334 if files is None:
1332 if files is None:
1335 files = {}
1333 files = {}
1336 if eolmode is None:
1334 if eolmode is None:
1337 eolmode = ui.config('patch', 'eol', 'strict')
1335 eolmode = ui.config('patch', 'eol', 'strict')
1338 if eolmode.lower() not in eolmodes:
1336 if eolmode.lower() not in eolmodes:
1339 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1337 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1340 eolmode = eolmode.lower()
1338 eolmode = eolmode.lower()
1341
1339
1342 backend = workingbackend(ui, repo, similarity)
1340 backend = workingbackend(ui, repo, similarity)
1343 try:
1341 try:
1344 fp = open(patchobj, 'rb')
1342 fp = open(patchobj, 'rb')
1345 except TypeError:
1343 except TypeError:
1346 fp = patchobj
1344 fp = patchobj
1347 try:
1345 try:
1348 ret = applydiff(ui, fp, files, backend, strip=strip, eolmode=eolmode)
1346 ret = applydiff(ui, fp, files, backend, strip=strip, eolmode=eolmode)
1349 finally:
1347 finally:
1350 if fp != patchobj:
1348 if fp != patchobj:
1351 fp.close()
1349 fp.close()
1352 files.update(dict.fromkeys(backend.close()))
1350 files.update(dict.fromkeys(backend.close()))
1353 if ret < 0:
1351 if ret < 0:
1354 raise PatchError(_('patch failed to apply'))
1352 raise PatchError(_('patch failed to apply'))
1355 return ret > 0
1353 return ret > 0
1356
1354
1357 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1355 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1358 similarity=0):
1356 similarity=0):
1359 """Apply <patchname> to the working directory.
1357 """Apply <patchname> to the working directory.
1360
1358
1361 'eolmode' specifies how end of lines should be handled. It can be:
1359 'eolmode' specifies how end of lines should be handled. It can be:
1362 - 'strict': inputs are read in binary mode, EOLs are preserved
1360 - 'strict': inputs are read in binary mode, EOLs are preserved
1363 - 'crlf': EOLs are ignored when patching and reset to CRLF
1361 - 'crlf': EOLs are ignored when patching and reset to CRLF
1364 - 'lf': EOLs are ignored when patching and reset to LF
1362 - 'lf': EOLs are ignored when patching and reset to LF
1365 - None: get it from user settings, default to 'strict'
1363 - None: get it from user settings, default to 'strict'
1366 'eolmode' is ignored when using an external patcher program.
1364 'eolmode' is ignored when using an external patcher program.
1367
1365
1368 Returns whether patch was applied with fuzz factor.
1366 Returns whether patch was applied with fuzz factor.
1369 """
1367 """
1370 patcher = ui.config('ui', 'patch')
1368 patcher = ui.config('ui', 'patch')
1371 if files is None:
1369 if files is None:
1372 files = {}
1370 files = {}
1373 try:
1371 try:
1374 if patcher:
1372 if patcher:
1375 return _externalpatch(ui, repo, patcher, patchname, strip,
1373 return _externalpatch(ui, repo, patcher, patchname, strip,
1376 files, similarity)
1374 files, similarity)
1377 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1375 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1378 similarity)
1376 similarity)
1379 except PatchError, err:
1377 except PatchError, err:
1380 raise util.Abort(str(err))
1378 raise util.Abort(str(err))
1381
1379
1382 def changedfiles(ui, repo, patchpath, strip=1):
1380 def changedfiles(ui, repo, patchpath, strip=1):
1383 backend = fsbackend(ui, repo.root)
1381 backend = fsbackend(ui, repo.root)
1384 fp = open(patchpath, 'rb')
1382 fp = open(patchpath, 'rb')
1385 try:
1383 try:
1386 changed = set()
1384 changed = set()
1387 for state, values in iterhunks(fp):
1385 for state, values in iterhunks(fp):
1388 if state == 'hunk':
1386 if state == 'hunk':
1389 continue
1387 continue
1390 elif state == 'file':
1388 elif state == 'file':
1391 afile, bfile, first_hunk, mode = values
1389 afile, bfile, first_hunk, mode = values
1392 current_file, missing = selectfile(backend, afile, bfile,
1390 current_file, missing = selectfile(backend, afile, bfile,
1393 first_hunk, strip)
1391 first_hunk, strip)
1394 changed.add(current_file)
1392 changed.add(current_file)
1395 elif state == 'git':
1393 elif state == 'git':
1396 for gp in values:
1394 for gp in values:
1397 gp.path = pathstrip(gp.path, strip - 1)[1]
1395 gp.path = pathstrip(gp.path, strip - 1)[1]
1398 changed.add(gp.path)
1396 changed.add(gp.path)
1399 if gp.oldpath:
1397 if gp.oldpath:
1400 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1398 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1401 if gp.op == 'RENAME':
1399 if gp.op == 'RENAME':
1402 changed.add(gp.oldpath)
1400 changed.add(gp.oldpath)
1403 else:
1401 else:
1404 raise util.Abort(_('unsupported parser state: %s') % state)
1402 raise util.Abort(_('unsupported parser state: %s') % state)
1405 return changed
1403 return changed
1406 finally:
1404 finally:
1407 fp.close()
1405 fp.close()
1408
1406
1409 def b85diff(to, tn):
1407 def b85diff(to, tn):
1410 '''print base85-encoded binary diff'''
1408 '''print base85-encoded binary diff'''
1411 def gitindex(text):
1409 def gitindex(text):
1412 if not text:
1410 if not text:
1413 return hex(nullid)
1411 return hex(nullid)
1414 l = len(text)
1412 l = len(text)
1415 s = util.sha1('blob %d\0' % l)
1413 s = util.sha1('blob %d\0' % l)
1416 s.update(text)
1414 s.update(text)
1417 return s.hexdigest()
1415 return s.hexdigest()
1418
1416
1419 def fmtline(line):
1417 def fmtline(line):
1420 l = len(line)
1418 l = len(line)
1421 if l <= 26:
1419 if l <= 26:
1422 l = chr(ord('A') + l - 1)
1420 l = chr(ord('A') + l - 1)
1423 else:
1421 else:
1424 l = chr(l - 26 + ord('a') - 1)
1422 l = chr(l - 26 + ord('a') - 1)
1425 return '%c%s\n' % (l, base85.b85encode(line, True))
1423 return '%c%s\n' % (l, base85.b85encode(line, True))
1426
1424
1427 def chunk(text, csize=52):
1425 def chunk(text, csize=52):
1428 l = len(text)
1426 l = len(text)
1429 i = 0
1427 i = 0
1430 while i < l:
1428 while i < l:
1431 yield text[i:i + csize]
1429 yield text[i:i + csize]
1432 i += csize
1430 i += csize
1433
1431
1434 tohash = gitindex(to)
1432 tohash = gitindex(to)
1435 tnhash = gitindex(tn)
1433 tnhash = gitindex(tn)
1436 if tohash == tnhash:
1434 if tohash == tnhash:
1437 return ""
1435 return ""
1438
1436
1439 # TODO: deltas
1437 # TODO: deltas
1440 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1438 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1441 (tohash, tnhash, len(tn))]
1439 (tohash, tnhash, len(tn))]
1442 for l in chunk(zlib.compress(tn)):
1440 for l in chunk(zlib.compress(tn)):
1443 ret.append(fmtline(l))
1441 ret.append(fmtline(l))
1444 ret.append('\n')
1442 ret.append('\n')
1445 return ''.join(ret)
1443 return ''.join(ret)
1446
1444
1447 class GitDiffRequired(Exception):
1445 class GitDiffRequired(Exception):
1448 pass
1446 pass
1449
1447
1450 def diffopts(ui, opts=None, untrusted=False):
1448 def diffopts(ui, opts=None, untrusted=False):
1451 def get(key, name=None, getter=ui.configbool):
1449 def get(key, name=None, getter=ui.configbool):
1452 return ((opts and opts.get(key)) or
1450 return ((opts and opts.get(key)) or
1453 getter('diff', name or key, None, untrusted=untrusted))
1451 getter('diff', name or key, None, untrusted=untrusted))
1454 return mdiff.diffopts(
1452 return mdiff.diffopts(
1455 text=opts and opts.get('text'),
1453 text=opts and opts.get('text'),
1456 git=get('git'),
1454 git=get('git'),
1457 nodates=get('nodates'),
1455 nodates=get('nodates'),
1458 showfunc=get('show_function', 'showfunc'),
1456 showfunc=get('show_function', 'showfunc'),
1459 ignorews=get('ignore_all_space', 'ignorews'),
1457 ignorews=get('ignore_all_space', 'ignorews'),
1460 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1458 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1461 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1459 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1462 context=get('unified', getter=ui.config))
1460 context=get('unified', getter=ui.config))
1463
1461
1464 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1462 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1465 losedatafn=None, prefix=''):
1463 losedatafn=None, prefix=''):
1466 '''yields diff of changes to files between two nodes, or node and
1464 '''yields diff of changes to files between two nodes, or node and
1467 working directory.
1465 working directory.
1468
1466
1469 if node1 is None, use first dirstate parent instead.
1467 if node1 is None, use first dirstate parent instead.
1470 if node2 is None, compare node1 with working directory.
1468 if node2 is None, compare node1 with working directory.
1471
1469
1472 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1470 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1473 every time some change cannot be represented with the current
1471 every time some change cannot be represented with the current
1474 patch format. Return False to upgrade to git patch format, True to
1472 patch format. Return False to upgrade to git patch format, True to
1475 accept the loss or raise an exception to abort the diff. It is
1473 accept the loss or raise an exception to abort the diff. It is
1476 called with the name of current file being diffed as 'fn'. If set
1474 called with the name of current file being diffed as 'fn'. If set
1477 to None, patches will always be upgraded to git format when
1475 to None, patches will always be upgraded to git format when
1478 necessary.
1476 necessary.
1479
1477
1480 prefix is a filename prefix that is prepended to all filenames on
1478 prefix is a filename prefix that is prepended to all filenames on
1481 display (used for subrepos).
1479 display (used for subrepos).
1482 '''
1480 '''
1483
1481
1484 if opts is None:
1482 if opts is None:
1485 opts = mdiff.defaultopts
1483 opts = mdiff.defaultopts
1486
1484
1487 if not node1 and not node2:
1485 if not node1 and not node2:
1488 node1 = repo.dirstate.p1()
1486 node1 = repo.dirstate.p1()
1489
1487
1490 def lrugetfilectx():
1488 def lrugetfilectx():
1491 cache = {}
1489 cache = {}
1492 order = []
1490 order = []
1493 def getfilectx(f, ctx):
1491 def getfilectx(f, ctx):
1494 fctx = ctx.filectx(f, filelog=cache.get(f))
1492 fctx = ctx.filectx(f, filelog=cache.get(f))
1495 if f not in cache:
1493 if f not in cache:
1496 if len(cache) > 20:
1494 if len(cache) > 20:
1497 del cache[order.pop(0)]
1495 del cache[order.pop(0)]
1498 cache[f] = fctx.filelog()
1496 cache[f] = fctx.filelog()
1499 else:
1497 else:
1500 order.remove(f)
1498 order.remove(f)
1501 order.append(f)
1499 order.append(f)
1502 return fctx
1500 return fctx
1503 return getfilectx
1501 return getfilectx
1504 getfilectx = lrugetfilectx()
1502 getfilectx = lrugetfilectx()
1505
1503
1506 ctx1 = repo[node1]
1504 ctx1 = repo[node1]
1507 ctx2 = repo[node2]
1505 ctx2 = repo[node2]
1508
1506
1509 if not changes:
1507 if not changes:
1510 changes = repo.status(ctx1, ctx2, match=match)
1508 changes = repo.status(ctx1, ctx2, match=match)
1511 modified, added, removed = changes[:3]
1509 modified, added, removed = changes[:3]
1512
1510
1513 if not modified and not added and not removed:
1511 if not modified and not added and not removed:
1514 return []
1512 return []
1515
1513
1516 revs = None
1514 revs = None
1517 if not repo.ui.quiet:
1515 if not repo.ui.quiet:
1518 hexfunc = repo.ui.debugflag and hex or short
1516 hexfunc = repo.ui.debugflag and hex or short
1519 revs = [hexfunc(node) for node in [node1, node2] if node]
1517 revs = [hexfunc(node) for node in [node1, node2] if node]
1520
1518
1521 copy = {}
1519 copy = {}
1522 if opts.git or opts.upgrade:
1520 if opts.git or opts.upgrade:
1523 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1521 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1524
1522
1525 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1523 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1526 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1524 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1527 if opts.upgrade and not opts.git:
1525 if opts.upgrade and not opts.git:
1528 try:
1526 try:
1529 def losedata(fn):
1527 def losedata(fn):
1530 if not losedatafn or not losedatafn(fn=fn):
1528 if not losedatafn or not losedatafn(fn=fn):
1531 raise GitDiffRequired()
1529 raise GitDiffRequired()
1532 # Buffer the whole output until we are sure it can be generated
1530 # Buffer the whole output until we are sure it can be generated
1533 return list(difffn(opts.copy(git=False), losedata))
1531 return list(difffn(opts.copy(git=False), losedata))
1534 except GitDiffRequired:
1532 except GitDiffRequired:
1535 return difffn(opts.copy(git=True), None)
1533 return difffn(opts.copy(git=True), None)
1536 else:
1534 else:
1537 return difffn(opts, None)
1535 return difffn(opts, None)
1538
1536
1539 def difflabel(func, *args, **kw):
1537 def difflabel(func, *args, **kw):
1540 '''yields 2-tuples of (output, label) based on the output of func()'''
1538 '''yields 2-tuples of (output, label) based on the output of func()'''
1541 prefixes = [('diff', 'diff.diffline'),
1539 prefixes = [('diff', 'diff.diffline'),
1542 ('copy', 'diff.extended'),
1540 ('copy', 'diff.extended'),
1543 ('rename', 'diff.extended'),
1541 ('rename', 'diff.extended'),
1544 ('old', 'diff.extended'),
1542 ('old', 'diff.extended'),
1545 ('new', 'diff.extended'),
1543 ('new', 'diff.extended'),
1546 ('deleted', 'diff.extended'),
1544 ('deleted', 'diff.extended'),
1547 ('---', 'diff.file_a'),
1545 ('---', 'diff.file_a'),
1548 ('+++', 'diff.file_b'),
1546 ('+++', 'diff.file_b'),
1549 ('@@', 'diff.hunk'),
1547 ('@@', 'diff.hunk'),
1550 ('-', 'diff.deleted'),
1548 ('-', 'diff.deleted'),
1551 ('+', 'diff.inserted')]
1549 ('+', 'diff.inserted')]
1552
1550
1553 for chunk in func(*args, **kw):
1551 for chunk in func(*args, **kw):
1554 lines = chunk.split('\n')
1552 lines = chunk.split('\n')
1555 for i, line in enumerate(lines):
1553 for i, line in enumerate(lines):
1556 if i != 0:
1554 if i != 0:
1557 yield ('\n', '')
1555 yield ('\n', '')
1558 stripline = line
1556 stripline = line
1559 if line and line[0] in '+-':
1557 if line and line[0] in '+-':
1560 # highlight trailing whitespace, but only in changed lines
1558 # highlight trailing whitespace, but only in changed lines
1561 stripline = line.rstrip()
1559 stripline = line.rstrip()
1562 for prefix, label in prefixes:
1560 for prefix, label in prefixes:
1563 if stripline.startswith(prefix):
1561 if stripline.startswith(prefix):
1564 yield (stripline, label)
1562 yield (stripline, label)
1565 break
1563 break
1566 else:
1564 else:
1567 yield (line, '')
1565 yield (line, '')
1568 if line != stripline:
1566 if line != stripline:
1569 yield (line[len(stripline):], 'diff.trailingwhitespace')
1567 yield (line[len(stripline):], 'diff.trailingwhitespace')
1570
1568
1571 def diffui(*args, **kw):
1569 def diffui(*args, **kw):
1572 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1570 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1573 return difflabel(diff, *args, **kw)
1571 return difflabel(diff, *args, **kw)
1574
1572
1575
1573
1576 def _addmodehdr(header, omode, nmode):
1574 def _addmodehdr(header, omode, nmode):
1577 if omode != nmode:
1575 if omode != nmode:
1578 header.append('old mode %s\n' % omode)
1576 header.append('old mode %s\n' % omode)
1579 header.append('new mode %s\n' % nmode)
1577 header.append('new mode %s\n' % nmode)
1580
1578
1581 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1579 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1582 copy, getfilectx, opts, losedatafn, prefix):
1580 copy, getfilectx, opts, losedatafn, prefix):
1583
1581
1584 def join(f):
1582 def join(f):
1585 return os.path.join(prefix, f)
1583 return os.path.join(prefix, f)
1586
1584
1587 date1 = util.datestr(ctx1.date())
1585 date1 = util.datestr(ctx1.date())
1588 man1 = ctx1.manifest()
1586 man1 = ctx1.manifest()
1589
1587
1590 gone = set()
1588 gone = set()
1591 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1589 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1592
1590
1593 copyto = dict([(v, k) for k, v in copy.items()])
1591 copyto = dict([(v, k) for k, v in copy.items()])
1594
1592
1595 if opts.git:
1593 if opts.git:
1596 revs = None
1594 revs = None
1597
1595
1598 for f in sorted(modified + added + removed):
1596 for f in sorted(modified + added + removed):
1599 to = None
1597 to = None
1600 tn = None
1598 tn = None
1601 dodiff = True
1599 dodiff = True
1602 header = []
1600 header = []
1603 if f in man1:
1601 if f in man1:
1604 to = getfilectx(f, ctx1).data()
1602 to = getfilectx(f, ctx1).data()
1605 if f not in removed:
1603 if f not in removed:
1606 tn = getfilectx(f, ctx2).data()
1604 tn = getfilectx(f, ctx2).data()
1607 a, b = f, f
1605 a, b = f, f
1608 if opts.git or losedatafn:
1606 if opts.git or losedatafn:
1609 if f in added:
1607 if f in added:
1610 mode = gitmode[ctx2.flags(f)]
1608 mode = gitmode[ctx2.flags(f)]
1611 if f in copy or f in copyto:
1609 if f in copy or f in copyto:
1612 if opts.git:
1610 if opts.git:
1613 if f in copy:
1611 if f in copy:
1614 a = copy[f]
1612 a = copy[f]
1615 else:
1613 else:
1616 a = copyto[f]
1614 a = copyto[f]
1617 omode = gitmode[man1.flags(a)]
1615 omode = gitmode[man1.flags(a)]
1618 _addmodehdr(header, omode, mode)
1616 _addmodehdr(header, omode, mode)
1619 if a in removed and a not in gone:
1617 if a in removed and a not in gone:
1620 op = 'rename'
1618 op = 'rename'
1621 gone.add(a)
1619 gone.add(a)
1622 else:
1620 else:
1623 op = 'copy'
1621 op = 'copy'
1624 header.append('%s from %s\n' % (op, join(a)))
1622 header.append('%s from %s\n' % (op, join(a)))
1625 header.append('%s to %s\n' % (op, join(f)))
1623 header.append('%s to %s\n' % (op, join(f)))
1626 to = getfilectx(a, ctx1).data()
1624 to = getfilectx(a, ctx1).data()
1627 else:
1625 else:
1628 losedatafn(f)
1626 losedatafn(f)
1629 else:
1627 else:
1630 if opts.git:
1628 if opts.git:
1631 header.append('new file mode %s\n' % mode)
1629 header.append('new file mode %s\n' % mode)
1632 elif ctx2.flags(f):
1630 elif ctx2.flags(f):
1633 losedatafn(f)
1631 losedatafn(f)
1634 # In theory, if tn was copied or renamed we should check
1632 # In theory, if tn was copied or renamed we should check
1635 # if the source is binary too but the copy record already
1633 # if the source is binary too but the copy record already
1636 # forces git mode.
1634 # forces git mode.
1637 if util.binary(tn):
1635 if util.binary(tn):
1638 if opts.git:
1636 if opts.git:
1639 dodiff = 'binary'
1637 dodiff = 'binary'
1640 else:
1638 else:
1641 losedatafn(f)
1639 losedatafn(f)
1642 if not opts.git and not tn:
1640 if not opts.git and not tn:
1643 # regular diffs cannot represent new empty file
1641 # regular diffs cannot represent new empty file
1644 losedatafn(f)
1642 losedatafn(f)
1645 elif f in removed:
1643 elif f in removed:
1646 if opts.git:
1644 if opts.git:
1647 # have we already reported a copy above?
1645 # have we already reported a copy above?
1648 if ((f in copy and copy[f] in added
1646 if ((f in copy and copy[f] in added
1649 and copyto[copy[f]] == f) or
1647 and copyto[copy[f]] == f) or
1650 (f in copyto and copyto[f] in added
1648 (f in copyto and copyto[f] in added
1651 and copy[copyto[f]] == f)):
1649 and copy[copyto[f]] == f)):
1652 dodiff = False
1650 dodiff = False
1653 else:
1651 else:
1654 header.append('deleted file mode %s\n' %
1652 header.append('deleted file mode %s\n' %
1655 gitmode[man1.flags(f)])
1653 gitmode[man1.flags(f)])
1656 elif not to or util.binary(to):
1654 elif not to or util.binary(to):
1657 # regular diffs cannot represent empty file deletion
1655 # regular diffs cannot represent empty file deletion
1658 losedatafn(f)
1656 losedatafn(f)
1659 else:
1657 else:
1660 oflag = man1.flags(f)
1658 oflag = man1.flags(f)
1661 nflag = ctx2.flags(f)
1659 nflag = ctx2.flags(f)
1662 binary = util.binary(to) or util.binary(tn)
1660 binary = util.binary(to) or util.binary(tn)
1663 if opts.git:
1661 if opts.git:
1664 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1662 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1665 if binary:
1663 if binary:
1666 dodiff = 'binary'
1664 dodiff = 'binary'
1667 elif binary or nflag != oflag:
1665 elif binary or nflag != oflag:
1668 losedatafn(f)
1666 losedatafn(f)
1669 if opts.git:
1667 if opts.git:
1670 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1668 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1671
1669
1672 if dodiff:
1670 if dodiff:
1673 if dodiff == 'binary':
1671 if dodiff == 'binary':
1674 text = b85diff(to, tn)
1672 text = b85diff(to, tn)
1675 else:
1673 else:
1676 text = mdiff.unidiff(to, date1,
1674 text = mdiff.unidiff(to, date1,
1677 # ctx2 date may be dynamic
1675 # ctx2 date may be dynamic
1678 tn, util.datestr(ctx2.date()),
1676 tn, util.datestr(ctx2.date()),
1679 join(a), join(b), revs, opts=opts)
1677 join(a), join(b), revs, opts=opts)
1680 if header and (text or len(header) > 1):
1678 if header and (text or len(header) > 1):
1681 yield ''.join(header)
1679 yield ''.join(header)
1682 if text:
1680 if text:
1683 yield text
1681 yield text
1684
1682
1685 def diffstatdata(lines):
1683 def diffstatdata(lines):
1686 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1684 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1687
1685
1688 filename, adds, removes = None, 0, 0
1686 filename, adds, removes = None, 0, 0
1689 for line in lines:
1687 for line in lines:
1690 if line.startswith('diff'):
1688 if line.startswith('diff'):
1691 if filename:
1689 if filename:
1692 isbinary = adds == 0 and removes == 0
1690 isbinary = adds == 0 and removes == 0
1693 yield (filename, adds, removes, isbinary)
1691 yield (filename, adds, removes, isbinary)
1694 # set numbers to 0 anyway when starting new file
1692 # set numbers to 0 anyway when starting new file
1695 adds, removes = 0, 0
1693 adds, removes = 0, 0
1696 if line.startswith('diff --git'):
1694 if line.startswith('diff --git'):
1697 filename = gitre.search(line).group(1)
1695 filename = gitre.search(line).group(1)
1698 elif line.startswith('diff -r'):
1696 elif line.startswith('diff -r'):
1699 # format: "diff -r ... -r ... filename"
1697 # format: "diff -r ... -r ... filename"
1700 filename = diffre.search(line).group(1)
1698 filename = diffre.search(line).group(1)
1701 elif line.startswith('+') and not line.startswith('+++'):
1699 elif line.startswith('+') and not line.startswith('+++'):
1702 adds += 1
1700 adds += 1
1703 elif line.startswith('-') and not line.startswith('---'):
1701 elif line.startswith('-') and not line.startswith('---'):
1704 removes += 1
1702 removes += 1
1705 if filename:
1703 if filename:
1706 isbinary = adds == 0 and removes == 0
1704 isbinary = adds == 0 and removes == 0
1707 yield (filename, adds, removes, isbinary)
1705 yield (filename, adds, removes, isbinary)
1708
1706
1709 def diffstat(lines, width=80, git=False):
1707 def diffstat(lines, width=80, git=False):
1710 output = []
1708 output = []
1711 stats = list(diffstatdata(lines))
1709 stats = list(diffstatdata(lines))
1712
1710
1713 maxtotal, maxname = 0, 0
1711 maxtotal, maxname = 0, 0
1714 totaladds, totalremoves = 0, 0
1712 totaladds, totalremoves = 0, 0
1715 hasbinary = False
1713 hasbinary = False
1716
1714
1717 sized = [(filename, adds, removes, isbinary, encoding.colwidth(filename))
1715 sized = [(filename, adds, removes, isbinary, encoding.colwidth(filename))
1718 for filename, adds, removes, isbinary in stats]
1716 for filename, adds, removes, isbinary in stats]
1719
1717
1720 for filename, adds, removes, isbinary, namewidth in sized:
1718 for filename, adds, removes, isbinary, namewidth in sized:
1721 totaladds += adds
1719 totaladds += adds
1722 totalremoves += removes
1720 totalremoves += removes
1723 maxname = max(maxname, namewidth)
1721 maxname = max(maxname, namewidth)
1724 maxtotal = max(maxtotal, adds + removes)
1722 maxtotal = max(maxtotal, adds + removes)
1725 if isbinary:
1723 if isbinary:
1726 hasbinary = True
1724 hasbinary = True
1727
1725
1728 countwidth = len(str(maxtotal))
1726 countwidth = len(str(maxtotal))
1729 if hasbinary and countwidth < 3:
1727 if hasbinary and countwidth < 3:
1730 countwidth = 3
1728 countwidth = 3
1731 graphwidth = width - countwidth - maxname - 6
1729 graphwidth = width - countwidth - maxname - 6
1732 if graphwidth < 10:
1730 if graphwidth < 10:
1733 graphwidth = 10
1731 graphwidth = 10
1734
1732
1735 def scale(i):
1733 def scale(i):
1736 if maxtotal <= graphwidth:
1734 if maxtotal <= graphwidth:
1737 return i
1735 return i
1738 # If diffstat runs out of room it doesn't print anything,
1736 # If diffstat runs out of room it doesn't print anything,
1739 # which isn't very useful, so always print at least one + or -
1737 # which isn't very useful, so always print at least one + or -
1740 # if there were at least some changes.
1738 # if there were at least some changes.
1741 return max(i * graphwidth // maxtotal, int(bool(i)))
1739 return max(i * graphwidth // maxtotal, int(bool(i)))
1742
1740
1743 for filename, adds, removes, isbinary, namewidth in sized:
1741 for filename, adds, removes, isbinary, namewidth in sized:
1744 if git and isbinary:
1742 if git and isbinary:
1745 count = 'Bin'
1743 count = 'Bin'
1746 else:
1744 else:
1747 count = adds + removes
1745 count = adds + removes
1748 pluses = '+' * scale(adds)
1746 pluses = '+' * scale(adds)
1749 minuses = '-' * scale(removes)
1747 minuses = '-' * scale(removes)
1750 output.append(' %s%s | %*s %s%s\n' %
1748 output.append(' %s%s | %*s %s%s\n' %
1751 (filename, ' ' * (maxname - namewidth),
1749 (filename, ' ' * (maxname - namewidth),
1752 countwidth, count,
1750 countwidth, count,
1753 pluses, minuses))
1751 pluses, minuses))
1754
1752
1755 if stats:
1753 if stats:
1756 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1754 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1757 % (len(stats), totaladds, totalremoves))
1755 % (len(stats), totaladds, totalremoves))
1758
1756
1759 return ''.join(output)
1757 return ''.join(output)
1760
1758
1761 def diffstatui(*args, **kw):
1759 def diffstatui(*args, **kw):
1762 '''like diffstat(), but yields 2-tuples of (output, label) for
1760 '''like diffstat(), but yields 2-tuples of (output, label) for
1763 ui.write()
1761 ui.write()
1764 '''
1762 '''
1765
1763
1766 for line in diffstat(*args, **kw).splitlines():
1764 for line in diffstat(*args, **kw).splitlines():
1767 if line and line[-1] in '+-':
1765 if line and line[-1] in '+-':
1768 name, graph = line.rsplit(' ', 1)
1766 name, graph = line.rsplit(' ', 1)
1769 yield (name + ' ', '')
1767 yield (name + ' ', '')
1770 m = re.search(r'\++', graph)
1768 m = re.search(r'\++', graph)
1771 if m:
1769 if m:
1772 yield (m.group(0), 'diffstat.inserted')
1770 yield (m.group(0), 'diffstat.inserted')
1773 m = re.search(r'-+', graph)
1771 m = re.search(r'-+', graph)
1774 if m:
1772 if m:
1775 yield (m.group(0), 'diffstat.deleted')
1773 yield (m.group(0), 'diffstat.deleted')
1776 else:
1774 else:
1777 yield (line, '')
1775 yield (line, '')
1778 yield ('\n', '')
1776 yield ('\n', '')
@@ -1,385 +1,404 b''
1
1
2 $ hg init
2 $ hg init
3
3
4 New file:
4 New file:
5
5
6 $ hg import -d "1000000 0" -mnew - <<EOF
6 $ hg import -d "1000000 0" -mnew - <<EOF
7 > diff --git a/new b/new
7 > diff --git a/new b/new
8 > new file mode 100644
8 > new file mode 100644
9 > index 0000000..7898192
9 > index 0000000..7898192
10 > --- /dev/null
10 > --- /dev/null
11 > +++ b/new
11 > +++ b/new
12 > @@ -0,0 +1 @@
12 > @@ -0,0 +1 @@
13 > +a
13 > +a
14 > EOF
14 > EOF
15 applying patch from stdin
15 applying patch from stdin
16
16
17 $ hg tip -q
17 $ hg tip -q
18 0:ae3ee40d2079
18 0:ae3ee40d2079
19
19
20 New empty file:
20 New empty file:
21
21
22 $ hg import -d "1000000 0" -mempty - <<EOF
22 $ hg import -d "1000000 0" -mempty - <<EOF
23 > diff --git a/empty b/empty
23 > diff --git a/empty b/empty
24 > new file mode 100644
24 > new file mode 100644
25 > EOF
25 > EOF
26 applying patch from stdin
26 applying patch from stdin
27
27
28 $ hg tip -q
28 $ hg tip -q
29 1:ab199dc869b5
29 1:ab199dc869b5
30
30
31 $ hg locate empty
31 $ hg locate empty
32 empty
32 empty
33
33
34 chmod +x:
34 chmod +x:
35
35
36 $ hg import -d "1000000 0" -msetx - <<EOF
36 $ hg import -d "1000000 0" -msetx - <<EOF
37 > diff --git a/new b/new
37 > diff --git a/new b/new
38 > old mode 100644
38 > old mode 100644
39 > new mode 100755
39 > new mode 100755
40 > EOF
40 > EOF
41 applying patch from stdin
41 applying patch from stdin
42
42
43 $ hg tip -q
43 $ hg tip -q
44 2:3a34410f282e
44 2:3a34410f282e
45
45
46 $ test -x new
46 $ test -x new
47
47
48 Copy:
48 Copy:
49
49
50 $ hg import -d "1000000 0" -mcopy - <<EOF
50 $ hg import -d "1000000 0" -mcopy - <<EOF
51 > diff --git a/new b/copy
51 > diff --git a/new b/copy
52 > old mode 100755
52 > old mode 100755
53 > new mode 100644
53 > new mode 100644
54 > similarity index 100%
54 > similarity index 100%
55 > copy from new
55 > copy from new
56 > copy to copy
56 > copy to copy
57 > diff --git a/new b/copyx
57 > diff --git a/new b/copyx
58 > similarity index 100%
58 > similarity index 100%
59 > copy from new
59 > copy from new
60 > copy to copyx
60 > copy to copyx
61 > EOF
61 > EOF
62 applying patch from stdin
62 applying patch from stdin
63
63
64 $ hg tip -q
64 $ hg tip -q
65 3:37bacb7ca14d
65 3:37bacb7ca14d
66
66
67 $ if "$TESTDIR/hghave" -q execbit; then
67 $ if "$TESTDIR/hghave" -q execbit; then
68 > test -f copy -a ! -x copy || echo bad
68 > test -f copy -a ! -x copy || echo bad
69 > test -x copyx || echo bad
69 > test -x copyx || echo bad
70 > else
70 > else
71 > test -f copy || echo bad
71 > test -f copy || echo bad
72 > fi
72 > fi
73
73
74 $ cat copy
74 $ cat copy
75 a
75 a
76
76
77 $ hg cat copy
77 $ hg cat copy
78 a
78 a
79
79
80 Rename:
80 Rename:
81
81
82 $ hg import -d "1000000 0" -mrename - <<EOF
82 $ hg import -d "1000000 0" -mrename - <<EOF
83 > diff --git a/copy b/rename
83 > diff --git a/copy b/rename
84 > similarity index 100%
84 > similarity index 100%
85 > rename from copy
85 > rename from copy
86 > rename to rename
86 > rename to rename
87 > EOF
87 > EOF
88 applying patch from stdin
88 applying patch from stdin
89
89
90 $ hg tip -q
90 $ hg tip -q
91 4:47b81a94361d
91 4:47b81a94361d
92
92
93 $ hg locate
93 $ hg locate
94 copyx
94 copyx
95 empty
95 empty
96 new
96 new
97 rename
97 rename
98
98
99 Delete:
99 Delete:
100
100
101 $ hg import -d "1000000 0" -mdelete - <<EOF
101 $ hg import -d "1000000 0" -mdelete - <<EOF
102 > diff --git a/copyx b/copyx
102 > diff --git a/copyx b/copyx
103 > deleted file mode 100755
103 > deleted file mode 100755
104 > index 7898192..0000000
104 > index 7898192..0000000
105 > --- a/copyx
105 > --- a/copyx
106 > +++ /dev/null
106 > +++ /dev/null
107 > @@ -1 +0,0 @@
107 > @@ -1 +0,0 @@
108 > -a
108 > -a
109 > EOF
109 > EOF
110 applying patch from stdin
110 applying patch from stdin
111
111
112 $ hg tip -q
112 $ hg tip -q
113 5:d9b001d98336
113 5:d9b001d98336
114
114
115 $ hg locate
115 $ hg locate
116 empty
116 empty
117 new
117 new
118 rename
118 rename
119
119
120 $ test -f copyx
120 $ test -f copyx
121 [1]
121 [1]
122
122
123 Regular diff:
123 Regular diff:
124
124
125 $ hg import -d "1000000 0" -mregular - <<EOF
125 $ hg import -d "1000000 0" -mregular - <<EOF
126 > diff --git a/rename b/rename
126 > diff --git a/rename b/rename
127 > index 7898192..72e1fe3 100644
127 > index 7898192..72e1fe3 100644
128 > --- a/rename
128 > --- a/rename
129 > +++ b/rename
129 > +++ b/rename
130 > @@ -1 +1,5 @@
130 > @@ -1 +1,5 @@
131 > a
131 > a
132 > +a
132 > +a
133 > +a
133 > +a
134 > +a
134 > +a
135 > +a
135 > +a
136 > EOF
136 > EOF
137 applying patch from stdin
137 applying patch from stdin
138
138
139 $ hg tip -q
139 $ hg tip -q
140 6:ebe901e7576b
140 6:ebe901e7576b
141
141
142 Copy and modify:
142 Copy and modify:
143
143
144 $ hg import -d "1000000 0" -mcopymod - <<EOF
144 $ hg import -d "1000000 0" -mcopymod - <<EOF
145 > diff --git a/rename b/copy2
145 > diff --git a/rename b/copy2
146 > similarity index 80%
146 > similarity index 80%
147 > copy from rename
147 > copy from rename
148 > copy to copy2
148 > copy to copy2
149 > index 72e1fe3..b53c148 100644
149 > index 72e1fe3..b53c148 100644
150 > --- a/rename
150 > --- a/rename
151 > +++ b/copy2
151 > +++ b/copy2
152 > @@ -1,5 +1,5 @@
152 > @@ -1,5 +1,5 @@
153 > a
153 > a
154 > a
154 > a
155 > -a
155 > -a
156 > +b
156 > +b
157 > a
157 > a
158 > a
158 > a
159 > EOF
159 > EOF
160 applying patch from stdin
160 applying patch from stdin
161
161
162 $ hg tip -q
162 $ hg tip -q
163 7:18f368958ecd
163 7:18f368958ecd
164
164
165 $ hg cat copy2
165 $ hg cat copy2
166 a
166 a
167 a
167 a
168 b
168 b
169 a
169 a
170 a
170 a
171
171
172 Rename and modify:
172 Rename and modify:
173
173
174 $ hg import -d "1000000 0" -mrenamemod - <<EOF
174 $ hg import -d "1000000 0" -mrenamemod - <<EOF
175 > diff --git a/copy2 b/rename2
175 > diff --git a/copy2 b/rename2
176 > similarity index 80%
176 > similarity index 80%
177 > rename from copy2
177 > rename from copy2
178 > rename to rename2
178 > rename to rename2
179 > index b53c148..8f81e29 100644
179 > index b53c148..8f81e29 100644
180 > --- a/copy2
180 > --- a/copy2
181 > +++ b/rename2
181 > +++ b/rename2
182 > @@ -1,5 +1,5 @@
182 > @@ -1,5 +1,5 @@
183 > a
183 > a
184 > a
184 > a
185 > b
185 > b
186 > -a
186 > -a
187 > +c
187 > +c
188 > a
188 > a
189 > EOF
189 > EOF
190 applying patch from stdin
190 applying patch from stdin
191
191
192 $ hg tip -q
192 $ hg tip -q
193 8:c32b0d7e6f44
193 8:c32b0d7e6f44
194
194
195 $ hg locate copy2
195 $ hg locate copy2
196 [1]
196 [1]
197 $ hg cat rename2
197 $ hg cat rename2
198 a
198 a
199 a
199 a
200 b
200 b
201 c
201 c
202 a
202 a
203
203
204 One file renamed multiple times:
204 One file renamed multiple times:
205
205
206 $ hg import -d "1000000 0" -mmultirenames - <<EOF
206 $ hg import -d "1000000 0" -mmultirenames - <<EOF
207 > diff --git a/rename2 b/rename3
207 > diff --git a/rename2 b/rename3
208 > rename from rename2
208 > rename from rename2
209 > rename to rename3
209 > rename to rename3
210 > diff --git a/rename2 b/rename3-2
210 > diff --git a/rename2 b/rename3-2
211 > rename from rename2
211 > rename from rename2
212 > rename to rename3-2
212 > rename to rename3-2
213 > EOF
213 > EOF
214 applying patch from stdin
214 applying patch from stdin
215
215
216 $ hg tip -q
216 $ hg tip -q
217 9:034a6bf95330
217 9:034a6bf95330
218
218
219 $ hg log -vr. --template '{rev} {files} / {file_copies}\n'
219 $ hg log -vr. --template '{rev} {files} / {file_copies}\n'
220 9 rename2 rename3 rename3-2 / rename3 (rename2)rename3-2 (rename2)
220 9 rename2 rename3 rename3-2 / rename3 (rename2)rename3-2 (rename2)
221
221
222 $ hg locate rename2 rename3 rename3-2
222 $ hg locate rename2 rename3 rename3-2
223 rename3
223 rename3
224 rename3-2
224 rename3-2
225
225
226 $ hg cat rename3
226 $ hg cat rename3
227 a
227 a
228 a
228 a
229 b
229 b
230 c
230 c
231 a
231 a
232
232
233 $ hg cat rename3-2
233 $ hg cat rename3-2
234 a
234 a
235 a
235 a
236 b
236 b
237 c
237 c
238 a
238 a
239
239
240 $ echo foo > foo
240 $ echo foo > foo
241 $ hg add foo
241 $ hg add foo
242 $ hg ci -m 'add foo'
242 $ hg ci -m 'add foo'
243
243
244 Binary files and regular patch hunks:
244 Binary files and regular patch hunks:
245
245
246 $ hg import -d "1000000 0" -m binaryregular - <<EOF
246 $ hg import -d "1000000 0" -m binaryregular - <<EOF
247 > diff --git a/binary b/binary
247 > diff --git a/binary b/binary
248 > new file mode 100644
248 > new file mode 100644
249 > index 0000000000000000000000000000000000000000..593f4708db84ac8fd0f5cc47c634f38c013fe9e4
249 > index 0000000000000000000000000000000000000000..593f4708db84ac8fd0f5cc47c634f38c013fe9e4
250 > GIT binary patch
250 > GIT binary patch
251 > literal 4
251 > literal 4
252 > Lc\${NkU|;|M00aO5
252 > Lc\${NkU|;|M00aO5
253 >
253 >
254 > diff --git a/foo b/foo2
254 > diff --git a/foo b/foo2
255 > rename from foo
255 > rename from foo
256 > rename to foo2
256 > rename to foo2
257 > EOF
257 > EOF
258 applying patch from stdin
258 applying patch from stdin
259
259
260 $ hg tip -q
260 $ hg tip -q
261 11:c39bce63e786
261 11:c39bce63e786
262
262
263 $ cat foo2
263 $ cat foo2
264 foo
264 foo
265
265
266 $ hg manifest --debug | grep binary
266 $ hg manifest --debug | grep binary
267 045c85ba38952325e126c70962cc0f9d9077bc67 644 binary
267 045c85ba38952325e126c70962cc0f9d9077bc67 644 binary
268
268
269 Multiple binary files:
269 Multiple binary files:
270
270
271 $ hg import -d "1000000 0" -m multibinary - <<EOF
271 $ hg import -d "1000000 0" -m multibinary - <<EOF
272 > diff --git a/mbinary1 b/mbinary1
272 > diff --git a/mbinary1 b/mbinary1
273 > new file mode 100644
273 > new file mode 100644
274 > index 0000000000000000000000000000000000000000..593f4708db84ac8fd0f5cc47c634f38c013fe9e4
274 > index 0000000000000000000000000000000000000000..593f4708db84ac8fd0f5cc47c634f38c013fe9e4
275 > GIT binary patch
275 > GIT binary patch
276 > literal 4
276 > literal 4
277 > Lc\${NkU|;|M00aO5
277 > Lc\${NkU|;|M00aO5
278 >
278 >
279 > diff --git a/mbinary2 b/mbinary2
279 > diff --git a/mbinary2 b/mbinary2
280 > new file mode 100644
280 > new file mode 100644
281 > index 0000000000000000000000000000000000000000..112363ac1917b417ffbd7f376ca786a1e5fa7490
281 > index 0000000000000000000000000000000000000000..112363ac1917b417ffbd7f376ca786a1e5fa7490
282 > GIT binary patch
282 > GIT binary patch
283 > literal 5
283 > literal 5
284 > Mc\${NkU|\`?^000jF3jhEB
284 > Mc\${NkU|\`?^000jF3jhEB
285 >
285 >
286 > EOF
286 > EOF
287 applying patch from stdin
287 applying patch from stdin
288
288
289 $ hg tip -q
289 $ hg tip -q
290 12:30b530085242
290 12:30b530085242
291
291
292 $ hg manifest --debug | grep mbinary
292 $ hg manifest --debug | grep mbinary
293 045c85ba38952325e126c70962cc0f9d9077bc67 644 mbinary1
293 045c85ba38952325e126c70962cc0f9d9077bc67 644 mbinary1
294 a874b471193996e7cb034bb301cac7bdaf3e3f46 644 mbinary2
294 a874b471193996e7cb034bb301cac7bdaf3e3f46 644 mbinary2
295
295
296 Filenames with spaces:
296 Filenames with spaces:
297
297
298 $ hg import -d "1000000 0" -m spaces - <<EOF
298 $ hg import -d "1000000 0" -m spaces - <<EOF
299 > diff --git a/foo bar b/foo bar
299 > diff --git a/foo bar b/foo bar
300 > new file mode 100644
300 > new file mode 100644
301 > index 0000000..257cc56
301 > index 0000000..257cc56
302 > --- /dev/null
302 > --- /dev/null
303 > +++ b/foo bar
303 > +++ b/foo bar
304 > @@ -0,0 +1 @@
304 > @@ -0,0 +1 @@
305 > +foo
305 > +foo
306 > EOF
306 > EOF
307 applying patch from stdin
307 applying patch from stdin
308
308
309 $ hg tip -q
309 $ hg tip -q
310 13:04750ef42fb3
310 13:04750ef42fb3
311
311
312 $ cat "foo bar"
312 $ cat "foo bar"
313 foo
313 foo
314
314
315 Copy then modify the original file:
315 Copy then modify the original file:
316
316
317 $ hg import -d "1000000 0" -m copy-mod-orig - <<EOF
317 $ hg import -d "1000000 0" -m copy-mod-orig - <<EOF
318 > diff --git a/foo2 b/foo2
318 > diff --git a/foo2 b/foo2
319 > index 257cc56..fe08ec6 100644
319 > index 257cc56..fe08ec6 100644
320 > --- a/foo2
320 > --- a/foo2
321 > +++ b/foo2
321 > +++ b/foo2
322 > @@ -1 +1,2 @@
322 > @@ -1 +1,2 @@
323 > foo
323 > foo
324 > +new line
324 > +new line
325 > diff --git a/foo2 b/foo3
325 > diff --git a/foo2 b/foo3
326 > similarity index 100%
326 > similarity index 100%
327 > copy from foo2
327 > copy from foo2
328 > copy to foo3
328 > copy to foo3
329 > EOF
329 > EOF
330 applying patch from stdin
330 applying patch from stdin
331
331
332 $ hg tip -q
332 $ hg tip -q
333 14:c4cd9cdeaa74
333 14:c4cd9cdeaa74
334
334
335 $ cat foo3
335 $ cat foo3
336 foo
336 foo
337
337
338 Move text file and patch as binary
338 Move text file and patch as binary
339
339
340 $ echo a > text2
340 $ echo a > text2
341 $ hg ci -Am0
341 $ hg ci -Am0
342 adding text2
342 adding text2
343 $ hg import -d "1000000 0" -m rename-as-binary - <<"EOF"
343 $ hg import -d "1000000 0" -m rename-as-binary - <<"EOF"
344 > diff --git a/text2 b/binary2
344 > diff --git a/text2 b/binary2
345 > rename from text2
345 > rename from text2
346 > rename to binary2
346 > rename to binary2
347 > index 78981922613b2afb6025042ff6bd878ac1994e85..10efcb362e9f3b3420fcfbfc0e37f3dc16e29757
347 > index 78981922613b2afb6025042ff6bd878ac1994e85..10efcb362e9f3b3420fcfbfc0e37f3dc16e29757
348 > GIT binary patch
348 > GIT binary patch
349 > literal 5
349 > literal 5
350 > Mc$`b*O5$Pw00T?_*Z=?k
350 > Mc$`b*O5$Pw00T?_*Z=?k
351 >
351 >
352 > EOF
352 > EOF
353 applying patch from stdin
353 applying patch from stdin
354
354
355 $ cat binary2
355 $ cat binary2
356 a
356 a
357 b
357 b
358 \x00 (no-eol) (esc)
358 \x00 (no-eol) (esc)
359
359
360 $ hg st --copies --change .
360 $ hg st --copies --change .
361 A binary2
361 A binary2
362 text2
362 text2
363 R text2
363 R text2
364 $ cd ..
364 $ cd ..
365
365
366 Consecutive import with renames (issue2459)
366 Consecutive import with renames (issue2459)
367
367
368 $ hg init issue2459
368 $ hg init issue2459
369 $ cd issue2459
369 $ cd issue2459
370 $ hg import --no-commit --force - <<EOF
370 $ hg import --no-commit --force - <<EOF
371 > diff --git a/a b/a
371 > diff --git a/a b/a
372 > new file mode 100644
372 > new file mode 100644
373 > EOF
373 > EOF
374 applying patch from stdin
374 applying patch from stdin
375 $ hg import --no-commit --force - <<EOF
375 $ hg import --no-commit --force - <<EOF
376 > diff --git a/a b/b
376 > diff --git a/a b/b
377 > rename from a
377 > rename from a
378 > rename to b
378 > rename to b
379 > EOF
379 > EOF
380 applying patch from stdin
380 applying patch from stdin
381 a has not been committed yet, so no copy data will be stored for b.
381 a has not been committed yet, so no copy data will be stored for b.
382 $ hg debugstate
382 $ hg debugstate
383 a 0 -1 unset b
383 a 0 -1 unset b
384 $ hg ci -m done
384 $ hg ci -m done
385 $ cd ..
385 $ cd ..
386
387 Renames and strip
388
389 $ hg init renameandstrip
390 $ cd renameandstrip
391 $ echo a > a
392 $ hg ci -Am adda
393 adding a
394 $ hg import --no-commit -p2 - <<EOF
395 > diff --git a/foo/a b/foo/b
396 > rename from foo/a
397 > rename to foo/b
398 > EOF
399 applying patch from stdin
400 $ hg st --copies
401 A b
402 a
403 R a
404 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now