##// END OF EJS Templates
patch: add support for git delta hunks...
Nicolas Vigier -
r20137:9f1d4323 default
parent child Browse files
Show More
@@ -1,1866 +1,1931 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, os, errno, re, posixpath
9 import cStringIO, email, os, errno, re, posixpath
10 import tempfile, zlib, shutil
10 import tempfile, zlib, shutil
11 # On python2.4 you have to import these by name or they fail to
11 # On python2.4 you have to import these by name or they fail to
12 # load. This was not a problem on Python 2.7.
12 # load. This was not a problem on Python 2.7.
13 import email.Generator
13 import email.Generator
14 import email.Parser
14 import email.Parser
15
15
16 from i18n import _
16 from i18n import _
17 from node import hex, short
17 from node import hex, short
18 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
18 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
19
19
20 gitre = re.compile('diff --git a/(.*) b/(.*)')
20 gitre = re.compile('diff --git a/(.*) b/(.*)')
21
21
22 class PatchError(Exception):
22 class PatchError(Exception):
23 pass
23 pass
24
24
25
25
26 # public functions
26 # public functions
27
27
28 def split(stream):
28 def split(stream):
29 '''return an iterator of individual patches from a stream'''
29 '''return an iterator of individual patches from a stream'''
30 def isheader(line, inheader):
30 def isheader(line, inheader):
31 if inheader and line[0] in (' ', '\t'):
31 if inheader and line[0] in (' ', '\t'):
32 # continuation
32 # continuation
33 return True
33 return True
34 if line[0] in (' ', '-', '+'):
34 if line[0] in (' ', '-', '+'):
35 # diff line - don't check for header pattern in there
35 # diff line - don't check for header pattern in there
36 return False
36 return False
37 l = line.split(': ', 1)
37 l = line.split(': ', 1)
38 return len(l) == 2 and ' ' not in l[0]
38 return len(l) == 2 and ' ' not in l[0]
39
39
40 def chunk(lines):
40 def chunk(lines):
41 return cStringIO.StringIO(''.join(lines))
41 return cStringIO.StringIO(''.join(lines))
42
42
43 def hgsplit(stream, cur):
43 def hgsplit(stream, cur):
44 inheader = True
44 inheader = True
45
45
46 for line in stream:
46 for line in stream:
47 if not line.strip():
47 if not line.strip():
48 inheader = False
48 inheader = False
49 if not inheader and line.startswith('# HG changeset patch'):
49 if not inheader and line.startswith('# HG changeset patch'):
50 yield chunk(cur)
50 yield chunk(cur)
51 cur = []
51 cur = []
52 inheader = True
52 inheader = True
53
53
54 cur.append(line)
54 cur.append(line)
55
55
56 if cur:
56 if cur:
57 yield chunk(cur)
57 yield chunk(cur)
58
58
59 def mboxsplit(stream, cur):
59 def mboxsplit(stream, cur):
60 for line in stream:
60 for line in stream:
61 if line.startswith('From '):
61 if line.startswith('From '):
62 for c in split(chunk(cur[1:])):
62 for c in split(chunk(cur[1:])):
63 yield c
63 yield c
64 cur = []
64 cur = []
65
65
66 cur.append(line)
66 cur.append(line)
67
67
68 if cur:
68 if cur:
69 for c in split(chunk(cur[1:])):
69 for c in split(chunk(cur[1:])):
70 yield c
70 yield c
71
71
72 def mimesplit(stream, cur):
72 def mimesplit(stream, cur):
73 def msgfp(m):
73 def msgfp(m):
74 fp = cStringIO.StringIO()
74 fp = cStringIO.StringIO()
75 g = email.Generator.Generator(fp, mangle_from_=False)
75 g = email.Generator.Generator(fp, mangle_from_=False)
76 g.flatten(m)
76 g.flatten(m)
77 fp.seek(0)
77 fp.seek(0)
78 return fp
78 return fp
79
79
80 for line in stream:
80 for line in stream:
81 cur.append(line)
81 cur.append(line)
82 c = chunk(cur)
82 c = chunk(cur)
83
83
84 m = email.Parser.Parser().parse(c)
84 m = email.Parser.Parser().parse(c)
85 if not m.is_multipart():
85 if not m.is_multipart():
86 yield msgfp(m)
86 yield msgfp(m)
87 else:
87 else:
88 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
88 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
89 for part in m.walk():
89 for part in m.walk():
90 ct = part.get_content_type()
90 ct = part.get_content_type()
91 if ct not in ok_types:
91 if ct not in ok_types:
92 continue
92 continue
93 yield msgfp(part)
93 yield msgfp(part)
94
94
95 def headersplit(stream, cur):
95 def headersplit(stream, cur):
96 inheader = False
96 inheader = False
97
97
98 for line in stream:
98 for line in stream:
99 if not inheader and isheader(line, inheader):
99 if not inheader and isheader(line, inheader):
100 yield chunk(cur)
100 yield chunk(cur)
101 cur = []
101 cur = []
102 inheader = True
102 inheader = True
103 if inheader and not isheader(line, inheader):
103 if inheader and not isheader(line, inheader):
104 inheader = False
104 inheader = False
105
105
106 cur.append(line)
106 cur.append(line)
107
107
108 if cur:
108 if cur:
109 yield chunk(cur)
109 yield chunk(cur)
110
110
111 def remainder(cur):
111 def remainder(cur):
112 yield chunk(cur)
112 yield chunk(cur)
113
113
114 class fiter(object):
114 class fiter(object):
115 def __init__(self, fp):
115 def __init__(self, fp):
116 self.fp = fp
116 self.fp = fp
117
117
118 def __iter__(self):
118 def __iter__(self):
119 return self
119 return self
120
120
121 def next(self):
121 def next(self):
122 l = self.fp.readline()
122 l = self.fp.readline()
123 if not l:
123 if not l:
124 raise StopIteration
124 raise StopIteration
125 return l
125 return l
126
126
127 inheader = False
127 inheader = False
128 cur = []
128 cur = []
129
129
130 mimeheaders = ['content-type']
130 mimeheaders = ['content-type']
131
131
132 if not util.safehasattr(stream, 'next'):
132 if not util.safehasattr(stream, 'next'):
133 # http responses, for example, have readline but not next
133 # http responses, for example, have readline but not next
134 stream = fiter(stream)
134 stream = fiter(stream)
135
135
136 for line in stream:
136 for line in stream:
137 cur.append(line)
137 cur.append(line)
138 if line.startswith('# HG changeset patch'):
138 if line.startswith('# HG changeset patch'):
139 return hgsplit(stream, cur)
139 return hgsplit(stream, cur)
140 elif line.startswith('From '):
140 elif line.startswith('From '):
141 return mboxsplit(stream, cur)
141 return mboxsplit(stream, cur)
142 elif isheader(line, inheader):
142 elif isheader(line, inheader):
143 inheader = True
143 inheader = True
144 if line.split(':', 1)[0].lower() in mimeheaders:
144 if line.split(':', 1)[0].lower() in mimeheaders:
145 # let email parser handle this
145 # let email parser handle this
146 return mimesplit(stream, cur)
146 return mimesplit(stream, cur)
147 elif line.startswith('--- ') and inheader:
147 elif line.startswith('--- ') and inheader:
148 # No evil headers seen by diff start, split by hand
148 # No evil headers seen by diff start, split by hand
149 return headersplit(stream, cur)
149 return headersplit(stream, cur)
150 # Not enough info, keep reading
150 # Not enough info, keep reading
151
151
152 # if we are here, we have a very plain patch
152 # if we are here, we have a very plain patch
153 return remainder(cur)
153 return remainder(cur)
154
154
155 def extract(ui, fileobj):
155 def extract(ui, fileobj):
156 '''extract patch from data read from fileobj.
156 '''extract patch from data read from fileobj.
157
157
158 patch can be a normal patch or contained in an email message.
158 patch can be a normal patch or contained in an email message.
159
159
160 return tuple (filename, message, user, date, branch, node, p1, p2).
160 return tuple (filename, message, user, date, branch, node, p1, p2).
161 Any item in the returned tuple can be None. If filename is None,
161 Any item in the returned tuple can be None. If filename is None,
162 fileobj did not contain a patch. Caller must unlink filename when done.'''
162 fileobj did not contain a patch. Caller must unlink filename when done.'''
163
163
164 # attempt to detect the start of a patch
164 # attempt to detect the start of a patch
165 # (this heuristic is borrowed from quilt)
165 # (this heuristic is borrowed from quilt)
166 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
166 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
167 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
167 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
168 r'---[ \t].*?^\+\+\+[ \t]|'
168 r'---[ \t].*?^\+\+\+[ \t]|'
169 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
169 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
170
170
171 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
171 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
172 tmpfp = os.fdopen(fd, 'w')
172 tmpfp = os.fdopen(fd, 'w')
173 try:
173 try:
174 msg = email.Parser.Parser().parse(fileobj)
174 msg = email.Parser.Parser().parse(fileobj)
175
175
176 subject = msg['Subject']
176 subject = msg['Subject']
177 user = msg['From']
177 user = msg['From']
178 if not subject and not user:
178 if not subject and not user:
179 # Not an email, restore parsed headers if any
179 # Not an email, restore parsed headers if any
180 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
180 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
181
181
182 # should try to parse msg['Date']
182 # should try to parse msg['Date']
183 date = None
183 date = None
184 nodeid = None
184 nodeid = None
185 branch = None
185 branch = None
186 parents = []
186 parents = []
187
187
188 if subject:
188 if subject:
189 if subject.startswith('[PATCH'):
189 if subject.startswith('[PATCH'):
190 pend = subject.find(']')
190 pend = subject.find(']')
191 if pend >= 0:
191 if pend >= 0:
192 subject = subject[pend + 1:].lstrip()
192 subject = subject[pend + 1:].lstrip()
193 subject = re.sub(r'\n[ \t]+', ' ', subject)
193 subject = re.sub(r'\n[ \t]+', ' ', subject)
194 ui.debug('Subject: %s\n' % subject)
194 ui.debug('Subject: %s\n' % subject)
195 if user:
195 if user:
196 ui.debug('From: %s\n' % user)
196 ui.debug('From: %s\n' % user)
197 diffs_seen = 0
197 diffs_seen = 0
198 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
198 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
199 message = ''
199 message = ''
200 for part in msg.walk():
200 for part in msg.walk():
201 content_type = part.get_content_type()
201 content_type = part.get_content_type()
202 ui.debug('Content-Type: %s\n' % content_type)
202 ui.debug('Content-Type: %s\n' % content_type)
203 if content_type not in ok_types:
203 if content_type not in ok_types:
204 continue
204 continue
205 payload = part.get_payload(decode=True)
205 payload = part.get_payload(decode=True)
206 m = diffre.search(payload)
206 m = diffre.search(payload)
207 if m:
207 if m:
208 hgpatch = False
208 hgpatch = False
209 hgpatchheader = False
209 hgpatchheader = False
210 ignoretext = False
210 ignoretext = False
211
211
212 ui.debug('found patch at byte %d\n' % m.start(0))
212 ui.debug('found patch at byte %d\n' % m.start(0))
213 diffs_seen += 1
213 diffs_seen += 1
214 cfp = cStringIO.StringIO()
214 cfp = cStringIO.StringIO()
215 for line in payload[:m.start(0)].splitlines():
215 for line in payload[:m.start(0)].splitlines():
216 if line.startswith('# HG changeset patch') and not hgpatch:
216 if line.startswith('# HG changeset patch') and not hgpatch:
217 ui.debug('patch generated by hg export\n')
217 ui.debug('patch generated by hg export\n')
218 hgpatch = True
218 hgpatch = True
219 hgpatchheader = True
219 hgpatchheader = True
220 # drop earlier commit message content
220 # drop earlier commit message content
221 cfp.seek(0)
221 cfp.seek(0)
222 cfp.truncate()
222 cfp.truncate()
223 subject = None
223 subject = None
224 elif hgpatchheader:
224 elif hgpatchheader:
225 if line.startswith('# User '):
225 if line.startswith('# User '):
226 user = line[7:]
226 user = line[7:]
227 ui.debug('From: %s\n' % user)
227 ui.debug('From: %s\n' % user)
228 elif line.startswith("# Date "):
228 elif line.startswith("# Date "):
229 date = line[7:]
229 date = line[7:]
230 elif line.startswith("# Branch "):
230 elif line.startswith("# Branch "):
231 branch = line[9:]
231 branch = line[9:]
232 elif line.startswith("# Node ID "):
232 elif line.startswith("# Node ID "):
233 nodeid = line[10:]
233 nodeid = line[10:]
234 elif line.startswith("# Parent "):
234 elif line.startswith("# Parent "):
235 parents.append(line[9:].lstrip())
235 parents.append(line[9:].lstrip())
236 elif not line.startswith("# "):
236 elif not line.startswith("# "):
237 hgpatchheader = False
237 hgpatchheader = False
238 elif line == '---':
238 elif line == '---':
239 ignoretext = True
239 ignoretext = True
240 if not hgpatchheader and not ignoretext:
240 if not hgpatchheader and not ignoretext:
241 cfp.write(line)
241 cfp.write(line)
242 cfp.write('\n')
242 cfp.write('\n')
243 message = cfp.getvalue()
243 message = cfp.getvalue()
244 if tmpfp:
244 if tmpfp:
245 tmpfp.write(payload)
245 tmpfp.write(payload)
246 if not payload.endswith('\n'):
246 if not payload.endswith('\n'):
247 tmpfp.write('\n')
247 tmpfp.write('\n')
248 elif not diffs_seen and message and content_type == 'text/plain':
248 elif not diffs_seen and message and content_type == 'text/plain':
249 message += '\n' + payload
249 message += '\n' + payload
250 except: # re-raises
250 except: # re-raises
251 tmpfp.close()
251 tmpfp.close()
252 os.unlink(tmpname)
252 os.unlink(tmpname)
253 raise
253 raise
254
254
255 if subject and not message.startswith(subject):
255 if subject and not message.startswith(subject):
256 message = '%s\n%s' % (subject, message)
256 message = '%s\n%s' % (subject, message)
257 tmpfp.close()
257 tmpfp.close()
258 if not diffs_seen:
258 if not diffs_seen:
259 os.unlink(tmpname)
259 os.unlink(tmpname)
260 return None, message, user, date, branch, None, None, None
260 return None, message, user, date, branch, None, None, None
261 p1 = parents and parents.pop(0) or None
261 p1 = parents and parents.pop(0) or None
262 p2 = parents and parents.pop(0) or None
262 p2 = parents and parents.pop(0) or None
263 return tmpname, message, user, date, branch, nodeid, p1, p2
263 return tmpname, message, user, date, branch, nodeid, p1, p2
264
264
265 class patchmeta(object):
265 class patchmeta(object):
266 """Patched file metadata
266 """Patched file metadata
267
267
268 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
268 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
269 or COPY. 'path' is patched file path. 'oldpath' is set to the
269 or COPY. 'path' is patched file path. 'oldpath' is set to the
270 origin file when 'op' is either COPY or RENAME, None otherwise. If
270 origin file when 'op' is either COPY or RENAME, None otherwise. If
271 file mode is changed, 'mode' is a tuple (islink, isexec) where
271 file mode is changed, 'mode' is a tuple (islink, isexec) where
272 'islink' is True if the file is a symlink and 'isexec' is True if
272 'islink' is True if the file is a symlink and 'isexec' is True if
273 the file is executable. Otherwise, 'mode' is None.
273 the file is executable. Otherwise, 'mode' is None.
274 """
274 """
275 def __init__(self, path):
275 def __init__(self, path):
276 self.path = path
276 self.path = path
277 self.oldpath = None
277 self.oldpath = None
278 self.mode = None
278 self.mode = None
279 self.op = 'MODIFY'
279 self.op = 'MODIFY'
280 self.binary = False
280 self.binary = False
281
281
282 def setmode(self, mode):
282 def setmode(self, mode):
283 islink = mode & 020000
283 islink = mode & 020000
284 isexec = mode & 0100
284 isexec = mode & 0100
285 self.mode = (islink, isexec)
285 self.mode = (islink, isexec)
286
286
287 def copy(self):
287 def copy(self):
288 other = patchmeta(self.path)
288 other = patchmeta(self.path)
289 other.oldpath = self.oldpath
289 other.oldpath = self.oldpath
290 other.mode = self.mode
290 other.mode = self.mode
291 other.op = self.op
291 other.op = self.op
292 other.binary = self.binary
292 other.binary = self.binary
293 return other
293 return other
294
294
295 def _ispatchinga(self, afile):
295 def _ispatchinga(self, afile):
296 if afile == '/dev/null':
296 if afile == '/dev/null':
297 return self.op == 'ADD'
297 return self.op == 'ADD'
298 return afile == 'a/' + (self.oldpath or self.path)
298 return afile == 'a/' + (self.oldpath or self.path)
299
299
300 def _ispatchingb(self, bfile):
300 def _ispatchingb(self, bfile):
301 if bfile == '/dev/null':
301 if bfile == '/dev/null':
302 return self.op == 'DELETE'
302 return self.op == 'DELETE'
303 return bfile == 'b/' + self.path
303 return bfile == 'b/' + self.path
304
304
305 def ispatching(self, afile, bfile):
305 def ispatching(self, afile, bfile):
306 return self._ispatchinga(afile) and self._ispatchingb(bfile)
306 return self._ispatchinga(afile) and self._ispatchingb(bfile)
307
307
308 def __repr__(self):
308 def __repr__(self):
309 return "<patchmeta %s %r>" % (self.op, self.path)
309 return "<patchmeta %s %r>" % (self.op, self.path)
310
310
311 def readgitpatch(lr):
311 def readgitpatch(lr):
312 """extract git-style metadata about patches from <patchname>"""
312 """extract git-style metadata about patches from <patchname>"""
313
313
314 # Filter patch for git information
314 # Filter patch for git information
315 gp = None
315 gp = None
316 gitpatches = []
316 gitpatches = []
317 for line in lr:
317 for line in lr:
318 line = line.rstrip(' \r\n')
318 line = line.rstrip(' \r\n')
319 if line.startswith('diff --git a/'):
319 if line.startswith('diff --git a/'):
320 m = gitre.match(line)
320 m = gitre.match(line)
321 if m:
321 if m:
322 if gp:
322 if gp:
323 gitpatches.append(gp)
323 gitpatches.append(gp)
324 dst = m.group(2)
324 dst = m.group(2)
325 gp = patchmeta(dst)
325 gp = patchmeta(dst)
326 elif gp:
326 elif gp:
327 if line.startswith('--- '):
327 if line.startswith('--- '):
328 gitpatches.append(gp)
328 gitpatches.append(gp)
329 gp = None
329 gp = None
330 continue
330 continue
331 if line.startswith('rename from '):
331 if line.startswith('rename from '):
332 gp.op = 'RENAME'
332 gp.op = 'RENAME'
333 gp.oldpath = line[12:]
333 gp.oldpath = line[12:]
334 elif line.startswith('rename to '):
334 elif line.startswith('rename to '):
335 gp.path = line[10:]
335 gp.path = line[10:]
336 elif line.startswith('copy from '):
336 elif line.startswith('copy from '):
337 gp.op = 'COPY'
337 gp.op = 'COPY'
338 gp.oldpath = line[10:]
338 gp.oldpath = line[10:]
339 elif line.startswith('copy to '):
339 elif line.startswith('copy to '):
340 gp.path = line[8:]
340 gp.path = line[8:]
341 elif line.startswith('deleted file'):
341 elif line.startswith('deleted file'):
342 gp.op = 'DELETE'
342 gp.op = 'DELETE'
343 elif line.startswith('new file mode '):
343 elif line.startswith('new file mode '):
344 gp.op = 'ADD'
344 gp.op = 'ADD'
345 gp.setmode(int(line[-6:], 8))
345 gp.setmode(int(line[-6:], 8))
346 elif line.startswith('new mode '):
346 elif line.startswith('new mode '):
347 gp.setmode(int(line[-6:], 8))
347 gp.setmode(int(line[-6:], 8))
348 elif line.startswith('GIT binary patch'):
348 elif line.startswith('GIT binary patch'):
349 gp.binary = True
349 gp.binary = True
350 if gp:
350 if gp:
351 gitpatches.append(gp)
351 gitpatches.append(gp)
352
352
353 return gitpatches
353 return gitpatches
354
354
355 class linereader(object):
355 class linereader(object):
356 # simple class to allow pushing lines back into the input stream
356 # simple class to allow pushing lines back into the input stream
357 def __init__(self, fp):
357 def __init__(self, fp):
358 self.fp = fp
358 self.fp = fp
359 self.buf = []
359 self.buf = []
360
360
361 def push(self, line):
361 def push(self, line):
362 if line is not None:
362 if line is not None:
363 self.buf.append(line)
363 self.buf.append(line)
364
364
365 def readline(self):
365 def readline(self):
366 if self.buf:
366 if self.buf:
367 l = self.buf[0]
367 l = self.buf[0]
368 del self.buf[0]
368 del self.buf[0]
369 return l
369 return l
370 return self.fp.readline()
370 return self.fp.readline()
371
371
372 def __iter__(self):
372 def __iter__(self):
373 while True:
373 while True:
374 l = self.readline()
374 l = self.readline()
375 if not l:
375 if not l:
376 break
376 break
377 yield l
377 yield l
378
378
379 class abstractbackend(object):
379 class abstractbackend(object):
380 def __init__(self, ui):
380 def __init__(self, ui):
381 self.ui = ui
381 self.ui = ui
382
382
383 def getfile(self, fname):
383 def getfile(self, fname):
384 """Return target file data and flags as a (data, (islink,
384 """Return target file data and flags as a (data, (islink,
385 isexec)) tuple.
385 isexec)) tuple.
386 """
386 """
387 raise NotImplementedError
387 raise NotImplementedError
388
388
389 def setfile(self, fname, data, mode, copysource):
389 def setfile(self, fname, data, mode, copysource):
390 """Write data to target file fname and set its mode. mode is a
390 """Write data to target file fname and set its mode. mode is a
391 (islink, isexec) tuple. If data is None, the file content should
391 (islink, isexec) tuple. If data is None, the file content should
392 be left unchanged. If the file is modified after being copied,
392 be left unchanged. If the file is modified after being copied,
393 copysource is set to the original file name.
393 copysource is set to the original file name.
394 """
394 """
395 raise NotImplementedError
395 raise NotImplementedError
396
396
397 def unlink(self, fname):
397 def unlink(self, fname):
398 """Unlink target file."""
398 """Unlink target file."""
399 raise NotImplementedError
399 raise NotImplementedError
400
400
401 def writerej(self, fname, failed, total, lines):
401 def writerej(self, fname, failed, total, lines):
402 """Write rejected lines for fname. total is the number of hunks
402 """Write rejected lines for fname. total is the number of hunks
403 which failed to apply and total the total number of hunks for this
403 which failed to apply and total the total number of hunks for this
404 files.
404 files.
405 """
405 """
406 pass
406 pass
407
407
408 def exists(self, fname):
408 def exists(self, fname):
409 raise NotImplementedError
409 raise NotImplementedError
410
410
411 class fsbackend(abstractbackend):
411 class fsbackend(abstractbackend):
412 def __init__(self, ui, basedir):
412 def __init__(self, ui, basedir):
413 super(fsbackend, self).__init__(ui)
413 super(fsbackend, self).__init__(ui)
414 self.opener = scmutil.opener(basedir)
414 self.opener = scmutil.opener(basedir)
415
415
416 def _join(self, f):
416 def _join(self, f):
417 return os.path.join(self.opener.base, f)
417 return os.path.join(self.opener.base, f)
418
418
419 def getfile(self, fname):
419 def getfile(self, fname):
420 path = self._join(fname)
420 path = self._join(fname)
421 if os.path.islink(path):
421 if os.path.islink(path):
422 return (os.readlink(path), (True, False))
422 return (os.readlink(path), (True, False))
423 isexec = False
423 isexec = False
424 try:
424 try:
425 isexec = os.lstat(path).st_mode & 0100 != 0
425 isexec = os.lstat(path).st_mode & 0100 != 0
426 except OSError, e:
426 except OSError, e:
427 if e.errno != errno.ENOENT:
427 if e.errno != errno.ENOENT:
428 raise
428 raise
429 return (self.opener.read(fname), (False, isexec))
429 return (self.opener.read(fname), (False, isexec))
430
430
431 def setfile(self, fname, data, mode, copysource):
431 def setfile(self, fname, data, mode, copysource):
432 islink, isexec = mode
432 islink, isexec = mode
433 if data is None:
433 if data is None:
434 util.setflags(self._join(fname), islink, isexec)
434 util.setflags(self._join(fname), islink, isexec)
435 return
435 return
436 if islink:
436 if islink:
437 self.opener.symlink(data, fname)
437 self.opener.symlink(data, fname)
438 else:
438 else:
439 self.opener.write(fname, data)
439 self.opener.write(fname, data)
440 if isexec:
440 if isexec:
441 util.setflags(self._join(fname), False, True)
441 util.setflags(self._join(fname), False, True)
442
442
443 def unlink(self, fname):
443 def unlink(self, fname):
444 util.unlinkpath(self._join(fname), ignoremissing=True)
444 util.unlinkpath(self._join(fname), ignoremissing=True)
445
445
446 def writerej(self, fname, failed, total, lines):
446 def writerej(self, fname, failed, total, lines):
447 fname = fname + ".rej"
447 fname = fname + ".rej"
448 self.ui.warn(
448 self.ui.warn(
449 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
449 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
450 (failed, total, fname))
450 (failed, total, fname))
451 fp = self.opener(fname, 'w')
451 fp = self.opener(fname, 'w')
452 fp.writelines(lines)
452 fp.writelines(lines)
453 fp.close()
453 fp.close()
454
454
455 def exists(self, fname):
455 def exists(self, fname):
456 return os.path.lexists(self._join(fname))
456 return os.path.lexists(self._join(fname))
457
457
458 class workingbackend(fsbackend):
458 class workingbackend(fsbackend):
459 def __init__(self, ui, repo, similarity):
459 def __init__(self, ui, repo, similarity):
460 super(workingbackend, self).__init__(ui, repo.root)
460 super(workingbackend, self).__init__(ui, repo.root)
461 self.repo = repo
461 self.repo = repo
462 self.similarity = similarity
462 self.similarity = similarity
463 self.removed = set()
463 self.removed = set()
464 self.changed = set()
464 self.changed = set()
465 self.copied = []
465 self.copied = []
466
466
467 def _checkknown(self, fname):
467 def _checkknown(self, fname):
468 if self.repo.dirstate[fname] == '?' and self.exists(fname):
468 if self.repo.dirstate[fname] == '?' and self.exists(fname):
469 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
469 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
470
470
471 def setfile(self, fname, data, mode, copysource):
471 def setfile(self, fname, data, mode, copysource):
472 self._checkknown(fname)
472 self._checkknown(fname)
473 super(workingbackend, self).setfile(fname, data, mode, copysource)
473 super(workingbackend, self).setfile(fname, data, mode, copysource)
474 if copysource is not None:
474 if copysource is not None:
475 self.copied.append((copysource, fname))
475 self.copied.append((copysource, fname))
476 self.changed.add(fname)
476 self.changed.add(fname)
477
477
478 def unlink(self, fname):
478 def unlink(self, fname):
479 self._checkknown(fname)
479 self._checkknown(fname)
480 super(workingbackend, self).unlink(fname)
480 super(workingbackend, self).unlink(fname)
481 self.removed.add(fname)
481 self.removed.add(fname)
482 self.changed.add(fname)
482 self.changed.add(fname)
483
483
484 def close(self):
484 def close(self):
485 wctx = self.repo[None]
485 wctx = self.repo[None]
486 changed = set(self.changed)
486 changed = set(self.changed)
487 for src, dst in self.copied:
487 for src, dst in self.copied:
488 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
488 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
489 if self.removed:
489 if self.removed:
490 wctx.forget(sorted(self.removed))
490 wctx.forget(sorted(self.removed))
491 for f in self.removed:
491 for f in self.removed:
492 if f not in self.repo.dirstate:
492 if f not in self.repo.dirstate:
493 # File was deleted and no longer belongs to the
493 # File was deleted and no longer belongs to the
494 # dirstate, it was probably marked added then
494 # dirstate, it was probably marked added then
495 # deleted, and should not be considered by
495 # deleted, and should not be considered by
496 # marktouched().
496 # marktouched().
497 changed.discard(f)
497 changed.discard(f)
498 if changed:
498 if changed:
499 scmutil.marktouched(self.repo, changed, self.similarity)
499 scmutil.marktouched(self.repo, changed, self.similarity)
500 return sorted(self.changed)
500 return sorted(self.changed)
501
501
502 class filestore(object):
502 class filestore(object):
503 def __init__(self, maxsize=None):
503 def __init__(self, maxsize=None):
504 self.opener = None
504 self.opener = None
505 self.files = {}
505 self.files = {}
506 self.created = 0
506 self.created = 0
507 self.maxsize = maxsize
507 self.maxsize = maxsize
508 if self.maxsize is None:
508 if self.maxsize is None:
509 self.maxsize = 4*(2**20)
509 self.maxsize = 4*(2**20)
510 self.size = 0
510 self.size = 0
511 self.data = {}
511 self.data = {}
512
512
513 def setfile(self, fname, data, mode, copied=None):
513 def setfile(self, fname, data, mode, copied=None):
514 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
514 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
515 self.data[fname] = (data, mode, copied)
515 self.data[fname] = (data, mode, copied)
516 self.size += len(data)
516 self.size += len(data)
517 else:
517 else:
518 if self.opener is None:
518 if self.opener is None:
519 root = tempfile.mkdtemp(prefix='hg-patch-')
519 root = tempfile.mkdtemp(prefix='hg-patch-')
520 self.opener = scmutil.opener(root)
520 self.opener = scmutil.opener(root)
521 # Avoid filename issues with these simple names
521 # Avoid filename issues with these simple names
522 fn = str(self.created)
522 fn = str(self.created)
523 self.opener.write(fn, data)
523 self.opener.write(fn, data)
524 self.created += 1
524 self.created += 1
525 self.files[fname] = (fn, mode, copied)
525 self.files[fname] = (fn, mode, copied)
526
526
527 def getfile(self, fname):
527 def getfile(self, fname):
528 if fname in self.data:
528 if fname in self.data:
529 return self.data[fname]
529 return self.data[fname]
530 if not self.opener or fname not in self.files:
530 if not self.opener or fname not in self.files:
531 raise IOError
531 raise IOError
532 fn, mode, copied = self.files[fname]
532 fn, mode, copied = self.files[fname]
533 return self.opener.read(fn), mode, copied
533 return self.opener.read(fn), mode, copied
534
534
535 def close(self):
535 def close(self):
536 if self.opener:
536 if self.opener:
537 shutil.rmtree(self.opener.base)
537 shutil.rmtree(self.opener.base)
538
538
539 class repobackend(abstractbackend):
539 class repobackend(abstractbackend):
540 def __init__(self, ui, repo, ctx, store):
540 def __init__(self, ui, repo, ctx, store):
541 super(repobackend, self).__init__(ui)
541 super(repobackend, self).__init__(ui)
542 self.repo = repo
542 self.repo = repo
543 self.ctx = ctx
543 self.ctx = ctx
544 self.store = store
544 self.store = store
545 self.changed = set()
545 self.changed = set()
546 self.removed = set()
546 self.removed = set()
547 self.copied = {}
547 self.copied = {}
548
548
549 def _checkknown(self, fname):
549 def _checkknown(self, fname):
550 if fname not in self.ctx:
550 if fname not in self.ctx:
551 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
551 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
552
552
553 def getfile(self, fname):
553 def getfile(self, fname):
554 try:
554 try:
555 fctx = self.ctx[fname]
555 fctx = self.ctx[fname]
556 except error.LookupError:
556 except error.LookupError:
557 raise IOError
557 raise IOError
558 flags = fctx.flags()
558 flags = fctx.flags()
559 return fctx.data(), ('l' in flags, 'x' in flags)
559 return fctx.data(), ('l' in flags, 'x' in flags)
560
560
561 def setfile(self, fname, data, mode, copysource):
561 def setfile(self, fname, data, mode, copysource):
562 if copysource:
562 if copysource:
563 self._checkknown(copysource)
563 self._checkknown(copysource)
564 if data is None:
564 if data is None:
565 data = self.ctx[fname].data()
565 data = self.ctx[fname].data()
566 self.store.setfile(fname, data, mode, copysource)
566 self.store.setfile(fname, data, mode, copysource)
567 self.changed.add(fname)
567 self.changed.add(fname)
568 if copysource:
568 if copysource:
569 self.copied[fname] = copysource
569 self.copied[fname] = copysource
570
570
571 def unlink(self, fname):
571 def unlink(self, fname):
572 self._checkknown(fname)
572 self._checkknown(fname)
573 self.removed.add(fname)
573 self.removed.add(fname)
574
574
575 def exists(self, fname):
575 def exists(self, fname):
576 return fname in self.ctx
576 return fname in self.ctx
577
577
578 def close(self):
578 def close(self):
579 return self.changed | self.removed
579 return self.changed | self.removed
580
580
581 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
581 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
582 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
582 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
583 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
583 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
584 eolmodes = ['strict', 'crlf', 'lf', 'auto']
584 eolmodes = ['strict', 'crlf', 'lf', 'auto']
585
585
586 class patchfile(object):
586 class patchfile(object):
587 def __init__(self, ui, gp, backend, store, eolmode='strict'):
587 def __init__(self, ui, gp, backend, store, eolmode='strict'):
588 self.fname = gp.path
588 self.fname = gp.path
589 self.eolmode = eolmode
589 self.eolmode = eolmode
590 self.eol = None
590 self.eol = None
591 self.backend = backend
591 self.backend = backend
592 self.ui = ui
592 self.ui = ui
593 self.lines = []
593 self.lines = []
594 self.exists = False
594 self.exists = False
595 self.missing = True
595 self.missing = True
596 self.mode = gp.mode
596 self.mode = gp.mode
597 self.copysource = gp.oldpath
597 self.copysource = gp.oldpath
598 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
598 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
599 self.remove = gp.op == 'DELETE'
599 self.remove = gp.op == 'DELETE'
600 try:
600 try:
601 if self.copysource is None:
601 if self.copysource is None:
602 data, mode = backend.getfile(self.fname)
602 data, mode = backend.getfile(self.fname)
603 self.exists = True
603 self.exists = True
604 else:
604 else:
605 data, mode = store.getfile(self.copysource)[:2]
605 data, mode = store.getfile(self.copysource)[:2]
606 self.exists = backend.exists(self.fname)
606 self.exists = backend.exists(self.fname)
607 self.missing = False
607 self.missing = False
608 if data:
608 if data:
609 self.lines = mdiff.splitnewlines(data)
609 self.lines = mdiff.splitnewlines(data)
610 if self.mode is None:
610 if self.mode is None:
611 self.mode = mode
611 self.mode = mode
612 if self.lines:
612 if self.lines:
613 # Normalize line endings
613 # Normalize line endings
614 if self.lines[0].endswith('\r\n'):
614 if self.lines[0].endswith('\r\n'):
615 self.eol = '\r\n'
615 self.eol = '\r\n'
616 elif self.lines[0].endswith('\n'):
616 elif self.lines[0].endswith('\n'):
617 self.eol = '\n'
617 self.eol = '\n'
618 if eolmode != 'strict':
618 if eolmode != 'strict':
619 nlines = []
619 nlines = []
620 for l in self.lines:
620 for l in self.lines:
621 if l.endswith('\r\n'):
621 if l.endswith('\r\n'):
622 l = l[:-2] + '\n'
622 l = l[:-2] + '\n'
623 nlines.append(l)
623 nlines.append(l)
624 self.lines = nlines
624 self.lines = nlines
625 except IOError:
625 except IOError:
626 if self.create:
626 if self.create:
627 self.missing = False
627 self.missing = False
628 if self.mode is None:
628 if self.mode is None:
629 self.mode = (False, False)
629 self.mode = (False, False)
630 if self.missing:
630 if self.missing:
631 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
631 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
632
632
633 self.hash = {}
633 self.hash = {}
634 self.dirty = 0
634 self.dirty = 0
635 self.offset = 0
635 self.offset = 0
636 self.skew = 0
636 self.skew = 0
637 self.rej = []
637 self.rej = []
638 self.fileprinted = False
638 self.fileprinted = False
639 self.printfile(False)
639 self.printfile(False)
640 self.hunks = 0
640 self.hunks = 0
641
641
642 def writelines(self, fname, lines, mode):
642 def writelines(self, fname, lines, mode):
643 if self.eolmode == 'auto':
643 if self.eolmode == 'auto':
644 eol = self.eol
644 eol = self.eol
645 elif self.eolmode == 'crlf':
645 elif self.eolmode == 'crlf':
646 eol = '\r\n'
646 eol = '\r\n'
647 else:
647 else:
648 eol = '\n'
648 eol = '\n'
649
649
650 if self.eolmode != 'strict' and eol and eol != '\n':
650 if self.eolmode != 'strict' and eol and eol != '\n':
651 rawlines = []
651 rawlines = []
652 for l in lines:
652 for l in lines:
653 if l and l[-1] == '\n':
653 if l and l[-1] == '\n':
654 l = l[:-1] + eol
654 l = l[:-1] + eol
655 rawlines.append(l)
655 rawlines.append(l)
656 lines = rawlines
656 lines = rawlines
657
657
658 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
658 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
659
659
660 def printfile(self, warn):
660 def printfile(self, warn):
661 if self.fileprinted:
661 if self.fileprinted:
662 return
662 return
663 if warn or self.ui.verbose:
663 if warn or self.ui.verbose:
664 self.fileprinted = True
664 self.fileprinted = True
665 s = _("patching file %s\n") % self.fname
665 s = _("patching file %s\n") % self.fname
666 if warn:
666 if warn:
667 self.ui.warn(s)
667 self.ui.warn(s)
668 else:
668 else:
669 self.ui.note(s)
669 self.ui.note(s)
670
670
671
671
672 def findlines(self, l, linenum):
672 def findlines(self, l, linenum):
673 # looks through the hash and finds candidate lines. The
673 # looks through the hash and finds candidate lines. The
674 # result is a list of line numbers sorted based on distance
674 # result is a list of line numbers sorted based on distance
675 # from linenum
675 # from linenum
676
676
677 cand = self.hash.get(l, [])
677 cand = self.hash.get(l, [])
678 if len(cand) > 1:
678 if len(cand) > 1:
679 # resort our list of potentials forward then back.
679 # resort our list of potentials forward then back.
680 cand.sort(key=lambda x: abs(x - linenum))
680 cand.sort(key=lambda x: abs(x - linenum))
681 return cand
681 return cand
682
682
683 def write_rej(self):
683 def write_rej(self):
684 # our rejects are a little different from patch(1). This always
684 # our rejects are a little different from patch(1). This always
685 # creates rejects in the same form as the original patch. A file
685 # creates rejects in the same form as the original patch. A file
686 # header is inserted so that you can run the reject through patch again
686 # header is inserted so that you can run the reject through patch again
687 # without having to type the filename.
687 # without having to type the filename.
688 if not self.rej:
688 if not self.rej:
689 return
689 return
690 base = os.path.basename(self.fname)
690 base = os.path.basename(self.fname)
691 lines = ["--- %s\n+++ %s\n" % (base, base)]
691 lines = ["--- %s\n+++ %s\n" % (base, base)]
692 for x in self.rej:
692 for x in self.rej:
693 for l in x.hunk:
693 for l in x.hunk:
694 lines.append(l)
694 lines.append(l)
695 if l[-1] != '\n':
695 if l[-1] != '\n':
696 lines.append("\n\ No newline at end of file\n")
696 lines.append("\n\ No newline at end of file\n")
697 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
697 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
698
698
699 def apply(self, h):
699 def apply(self, h):
700 if not h.complete():
700 if not h.complete():
701 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
701 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
702 (h.number, h.desc, len(h.a), h.lena, len(h.b),
702 (h.number, h.desc, len(h.a), h.lena, len(h.b),
703 h.lenb))
703 h.lenb))
704
704
705 self.hunks += 1
705 self.hunks += 1
706
706
707 if self.missing:
707 if self.missing:
708 self.rej.append(h)
708 self.rej.append(h)
709 return -1
709 return -1
710
710
711 if self.exists and self.create:
711 if self.exists and self.create:
712 if self.copysource:
712 if self.copysource:
713 self.ui.warn(_("cannot create %s: destination already "
713 self.ui.warn(_("cannot create %s: destination already "
714 "exists\n" % self.fname))
714 "exists\n" % self.fname))
715 else:
715 else:
716 self.ui.warn(_("file %s already exists\n") % self.fname)
716 self.ui.warn(_("file %s already exists\n") % self.fname)
717 self.rej.append(h)
717 self.rej.append(h)
718 return -1
718 return -1
719
719
720 if isinstance(h, binhunk):
720 if isinstance(h, binhunk):
721 if self.remove:
721 if self.remove:
722 self.backend.unlink(self.fname)
722 self.backend.unlink(self.fname)
723 else:
723 else:
724 self.lines[:] = h.new()
724 l = h.new(self.lines)
725 self.offset += len(h.new())
725 self.lines[:] = l
726 self.offset += len(l)
726 self.dirty = True
727 self.dirty = True
727 return 0
728 return 0
728
729
729 horig = h
730 horig = h
730 if (self.eolmode in ('crlf', 'lf')
731 if (self.eolmode in ('crlf', 'lf')
731 or self.eolmode == 'auto' and self.eol):
732 or self.eolmode == 'auto' and self.eol):
732 # If new eols are going to be normalized, then normalize
733 # If new eols are going to be normalized, then normalize
733 # hunk data before patching. Otherwise, preserve input
734 # hunk data before patching. Otherwise, preserve input
734 # line-endings.
735 # line-endings.
735 h = h.getnormalized()
736 h = h.getnormalized()
736
737
737 # fast case first, no offsets, no fuzz
738 # fast case first, no offsets, no fuzz
738 old, oldstart, new, newstart = h.fuzzit(0, False)
739 old, oldstart, new, newstart = h.fuzzit(0, False)
739 oldstart += self.offset
740 oldstart += self.offset
740 orig_start = oldstart
741 orig_start = oldstart
741 # if there's skew we want to emit the "(offset %d lines)" even
742 # if there's skew we want to emit the "(offset %d lines)" even
742 # when the hunk cleanly applies at start + skew, so skip the
743 # when the hunk cleanly applies at start + skew, so skip the
743 # fast case code
744 # fast case code
744 if (self.skew == 0 and
745 if (self.skew == 0 and
745 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
746 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
746 if self.remove:
747 if self.remove:
747 self.backend.unlink(self.fname)
748 self.backend.unlink(self.fname)
748 else:
749 else:
749 self.lines[oldstart:oldstart + len(old)] = new
750 self.lines[oldstart:oldstart + len(old)] = new
750 self.offset += len(new) - len(old)
751 self.offset += len(new) - len(old)
751 self.dirty = True
752 self.dirty = True
752 return 0
753 return 0
753
754
754 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
755 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
755 self.hash = {}
756 self.hash = {}
756 for x, s in enumerate(self.lines):
757 for x, s in enumerate(self.lines):
757 self.hash.setdefault(s, []).append(x)
758 self.hash.setdefault(s, []).append(x)
758
759
759 for fuzzlen in xrange(3):
760 for fuzzlen in xrange(3):
760 for toponly in [True, False]:
761 for toponly in [True, False]:
761 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
762 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
762 oldstart = oldstart + self.offset + self.skew
763 oldstart = oldstart + self.offset + self.skew
763 oldstart = min(oldstart, len(self.lines))
764 oldstart = min(oldstart, len(self.lines))
764 if old:
765 if old:
765 cand = self.findlines(old[0][1:], oldstart)
766 cand = self.findlines(old[0][1:], oldstart)
766 else:
767 else:
767 # Only adding lines with no or fuzzed context, just
768 # Only adding lines with no or fuzzed context, just
768 # take the skew in account
769 # take the skew in account
769 cand = [oldstart]
770 cand = [oldstart]
770
771
771 for l in cand:
772 for l in cand:
772 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
773 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
773 self.lines[l : l + len(old)] = new
774 self.lines[l : l + len(old)] = new
774 self.offset += len(new) - len(old)
775 self.offset += len(new) - len(old)
775 self.skew = l - orig_start
776 self.skew = l - orig_start
776 self.dirty = True
777 self.dirty = True
777 offset = l - orig_start - fuzzlen
778 offset = l - orig_start - fuzzlen
778 if fuzzlen:
779 if fuzzlen:
779 msg = _("Hunk #%d succeeded at %d "
780 msg = _("Hunk #%d succeeded at %d "
780 "with fuzz %d "
781 "with fuzz %d "
781 "(offset %d lines).\n")
782 "(offset %d lines).\n")
782 self.printfile(True)
783 self.printfile(True)
783 self.ui.warn(msg %
784 self.ui.warn(msg %
784 (h.number, l + 1, fuzzlen, offset))
785 (h.number, l + 1, fuzzlen, offset))
785 else:
786 else:
786 msg = _("Hunk #%d succeeded at %d "
787 msg = _("Hunk #%d succeeded at %d "
787 "(offset %d lines).\n")
788 "(offset %d lines).\n")
788 self.ui.note(msg % (h.number, l + 1, offset))
789 self.ui.note(msg % (h.number, l + 1, offset))
789 return fuzzlen
790 return fuzzlen
790 self.printfile(True)
791 self.printfile(True)
791 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
792 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
792 self.rej.append(horig)
793 self.rej.append(horig)
793 return -1
794 return -1
794
795
795 def close(self):
796 def close(self):
796 if self.dirty:
797 if self.dirty:
797 self.writelines(self.fname, self.lines, self.mode)
798 self.writelines(self.fname, self.lines, self.mode)
798 self.write_rej()
799 self.write_rej()
799 return len(self.rej)
800 return len(self.rej)
800
801
801 class hunk(object):
802 class hunk(object):
802 def __init__(self, desc, num, lr, context):
803 def __init__(self, desc, num, lr, context):
803 self.number = num
804 self.number = num
804 self.desc = desc
805 self.desc = desc
805 self.hunk = [desc]
806 self.hunk = [desc]
806 self.a = []
807 self.a = []
807 self.b = []
808 self.b = []
808 self.starta = self.lena = None
809 self.starta = self.lena = None
809 self.startb = self.lenb = None
810 self.startb = self.lenb = None
810 if lr is not None:
811 if lr is not None:
811 if context:
812 if context:
812 self.read_context_hunk(lr)
813 self.read_context_hunk(lr)
813 else:
814 else:
814 self.read_unified_hunk(lr)
815 self.read_unified_hunk(lr)
815
816
816 def getnormalized(self):
817 def getnormalized(self):
817 """Return a copy with line endings normalized to LF."""
818 """Return a copy with line endings normalized to LF."""
818
819
819 def normalize(lines):
820 def normalize(lines):
820 nlines = []
821 nlines = []
821 for line in lines:
822 for line in lines:
822 if line.endswith('\r\n'):
823 if line.endswith('\r\n'):
823 line = line[:-2] + '\n'
824 line = line[:-2] + '\n'
824 nlines.append(line)
825 nlines.append(line)
825 return nlines
826 return nlines
826
827
827 # Dummy object, it is rebuilt manually
828 # Dummy object, it is rebuilt manually
828 nh = hunk(self.desc, self.number, None, None)
829 nh = hunk(self.desc, self.number, None, None)
829 nh.number = self.number
830 nh.number = self.number
830 nh.desc = self.desc
831 nh.desc = self.desc
831 nh.hunk = self.hunk
832 nh.hunk = self.hunk
832 nh.a = normalize(self.a)
833 nh.a = normalize(self.a)
833 nh.b = normalize(self.b)
834 nh.b = normalize(self.b)
834 nh.starta = self.starta
835 nh.starta = self.starta
835 nh.startb = self.startb
836 nh.startb = self.startb
836 nh.lena = self.lena
837 nh.lena = self.lena
837 nh.lenb = self.lenb
838 nh.lenb = self.lenb
838 return nh
839 return nh
839
840
840 def read_unified_hunk(self, lr):
841 def read_unified_hunk(self, lr):
841 m = unidesc.match(self.desc)
842 m = unidesc.match(self.desc)
842 if not m:
843 if not m:
843 raise PatchError(_("bad hunk #%d") % self.number)
844 raise PatchError(_("bad hunk #%d") % self.number)
844 self.starta, self.lena, self.startb, self.lenb = m.groups()
845 self.starta, self.lena, self.startb, self.lenb = m.groups()
845 if self.lena is None:
846 if self.lena is None:
846 self.lena = 1
847 self.lena = 1
847 else:
848 else:
848 self.lena = int(self.lena)
849 self.lena = int(self.lena)
849 if self.lenb is None:
850 if self.lenb is None:
850 self.lenb = 1
851 self.lenb = 1
851 else:
852 else:
852 self.lenb = int(self.lenb)
853 self.lenb = int(self.lenb)
853 self.starta = int(self.starta)
854 self.starta = int(self.starta)
854 self.startb = int(self.startb)
855 self.startb = int(self.startb)
855 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
856 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
856 self.b)
857 self.b)
857 # if we hit eof before finishing out the hunk, the last line will
858 # if we hit eof before finishing out the hunk, the last line will
858 # be zero length. Lets try to fix it up.
859 # be zero length. Lets try to fix it up.
859 while len(self.hunk[-1]) == 0:
860 while len(self.hunk[-1]) == 0:
860 del self.hunk[-1]
861 del self.hunk[-1]
861 del self.a[-1]
862 del self.a[-1]
862 del self.b[-1]
863 del self.b[-1]
863 self.lena -= 1
864 self.lena -= 1
864 self.lenb -= 1
865 self.lenb -= 1
865 self._fixnewline(lr)
866 self._fixnewline(lr)
866
867
867 def read_context_hunk(self, lr):
868 def read_context_hunk(self, lr):
868 self.desc = lr.readline()
869 self.desc = lr.readline()
869 m = contextdesc.match(self.desc)
870 m = contextdesc.match(self.desc)
870 if not m:
871 if not m:
871 raise PatchError(_("bad hunk #%d") % self.number)
872 raise PatchError(_("bad hunk #%d") % self.number)
872 self.starta, aend = m.groups()
873 self.starta, aend = m.groups()
873 self.starta = int(self.starta)
874 self.starta = int(self.starta)
874 if aend is None:
875 if aend is None:
875 aend = self.starta
876 aend = self.starta
876 self.lena = int(aend) - self.starta
877 self.lena = int(aend) - self.starta
877 if self.starta:
878 if self.starta:
878 self.lena += 1
879 self.lena += 1
879 for x in xrange(self.lena):
880 for x in xrange(self.lena):
880 l = lr.readline()
881 l = lr.readline()
881 if l.startswith('---'):
882 if l.startswith('---'):
882 # lines addition, old block is empty
883 # lines addition, old block is empty
883 lr.push(l)
884 lr.push(l)
884 break
885 break
885 s = l[2:]
886 s = l[2:]
886 if l.startswith('- ') or l.startswith('! '):
887 if l.startswith('- ') or l.startswith('! '):
887 u = '-' + s
888 u = '-' + s
888 elif l.startswith(' '):
889 elif l.startswith(' '):
889 u = ' ' + s
890 u = ' ' + s
890 else:
891 else:
891 raise PatchError(_("bad hunk #%d old text line %d") %
892 raise PatchError(_("bad hunk #%d old text line %d") %
892 (self.number, x))
893 (self.number, x))
893 self.a.append(u)
894 self.a.append(u)
894 self.hunk.append(u)
895 self.hunk.append(u)
895
896
896 l = lr.readline()
897 l = lr.readline()
897 if l.startswith('\ '):
898 if l.startswith('\ '):
898 s = self.a[-1][:-1]
899 s = self.a[-1][:-1]
899 self.a[-1] = s
900 self.a[-1] = s
900 self.hunk[-1] = s
901 self.hunk[-1] = s
901 l = lr.readline()
902 l = lr.readline()
902 m = contextdesc.match(l)
903 m = contextdesc.match(l)
903 if not m:
904 if not m:
904 raise PatchError(_("bad hunk #%d") % self.number)
905 raise PatchError(_("bad hunk #%d") % self.number)
905 self.startb, bend = m.groups()
906 self.startb, bend = m.groups()
906 self.startb = int(self.startb)
907 self.startb = int(self.startb)
907 if bend is None:
908 if bend is None:
908 bend = self.startb
909 bend = self.startb
909 self.lenb = int(bend) - self.startb
910 self.lenb = int(bend) - self.startb
910 if self.startb:
911 if self.startb:
911 self.lenb += 1
912 self.lenb += 1
912 hunki = 1
913 hunki = 1
913 for x in xrange(self.lenb):
914 for x in xrange(self.lenb):
914 l = lr.readline()
915 l = lr.readline()
915 if l.startswith('\ '):
916 if l.startswith('\ '):
916 # XXX: the only way to hit this is with an invalid line range.
917 # XXX: the only way to hit this is with an invalid line range.
917 # The no-eol marker is not counted in the line range, but I
918 # The no-eol marker is not counted in the line range, but I
918 # guess there are diff(1) out there which behave differently.
919 # guess there are diff(1) out there which behave differently.
919 s = self.b[-1][:-1]
920 s = self.b[-1][:-1]
920 self.b[-1] = s
921 self.b[-1] = s
921 self.hunk[hunki - 1] = s
922 self.hunk[hunki - 1] = s
922 continue
923 continue
923 if not l:
924 if not l:
924 # line deletions, new block is empty and we hit EOF
925 # line deletions, new block is empty and we hit EOF
925 lr.push(l)
926 lr.push(l)
926 break
927 break
927 s = l[2:]
928 s = l[2:]
928 if l.startswith('+ ') or l.startswith('! '):
929 if l.startswith('+ ') or l.startswith('! '):
929 u = '+' + s
930 u = '+' + s
930 elif l.startswith(' '):
931 elif l.startswith(' '):
931 u = ' ' + s
932 u = ' ' + s
932 elif len(self.b) == 0:
933 elif len(self.b) == 0:
933 # line deletions, new block is empty
934 # line deletions, new block is empty
934 lr.push(l)
935 lr.push(l)
935 break
936 break
936 else:
937 else:
937 raise PatchError(_("bad hunk #%d old text line %d") %
938 raise PatchError(_("bad hunk #%d old text line %d") %
938 (self.number, x))
939 (self.number, x))
939 self.b.append(s)
940 self.b.append(s)
940 while True:
941 while True:
941 if hunki >= len(self.hunk):
942 if hunki >= len(self.hunk):
942 h = ""
943 h = ""
943 else:
944 else:
944 h = self.hunk[hunki]
945 h = self.hunk[hunki]
945 hunki += 1
946 hunki += 1
946 if h == u:
947 if h == u:
947 break
948 break
948 elif h.startswith('-'):
949 elif h.startswith('-'):
949 continue
950 continue
950 else:
951 else:
951 self.hunk.insert(hunki - 1, u)
952 self.hunk.insert(hunki - 1, u)
952 break
953 break
953
954
954 if not self.a:
955 if not self.a:
955 # this happens when lines were only added to the hunk
956 # this happens when lines were only added to the hunk
956 for x in self.hunk:
957 for x in self.hunk:
957 if x.startswith('-') or x.startswith(' '):
958 if x.startswith('-') or x.startswith(' '):
958 self.a.append(x)
959 self.a.append(x)
959 if not self.b:
960 if not self.b:
960 # this happens when lines were only deleted from the hunk
961 # this happens when lines were only deleted from the hunk
961 for x in self.hunk:
962 for x in self.hunk:
962 if x.startswith('+') or x.startswith(' '):
963 if x.startswith('+') or x.startswith(' '):
963 self.b.append(x[1:])
964 self.b.append(x[1:])
964 # @@ -start,len +start,len @@
965 # @@ -start,len +start,len @@
965 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
966 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
966 self.startb, self.lenb)
967 self.startb, self.lenb)
967 self.hunk[0] = self.desc
968 self.hunk[0] = self.desc
968 self._fixnewline(lr)
969 self._fixnewline(lr)
969
970
970 def _fixnewline(self, lr):
971 def _fixnewline(self, lr):
971 l = lr.readline()
972 l = lr.readline()
972 if l.startswith('\ '):
973 if l.startswith('\ '):
973 diffhelpers.fix_newline(self.hunk, self.a, self.b)
974 diffhelpers.fix_newline(self.hunk, self.a, self.b)
974 else:
975 else:
975 lr.push(l)
976 lr.push(l)
976
977
977 def complete(self):
978 def complete(self):
978 return len(self.a) == self.lena and len(self.b) == self.lenb
979 return len(self.a) == self.lena and len(self.b) == self.lenb
979
980
980 def _fuzzit(self, old, new, fuzz, toponly):
981 def _fuzzit(self, old, new, fuzz, toponly):
981 # this removes context lines from the top and bottom of list 'l'. It
982 # this removes context lines from the top and bottom of list 'l'. It
982 # checks the hunk to make sure only context lines are removed, and then
983 # checks the hunk to make sure only context lines are removed, and then
983 # returns a new shortened list of lines.
984 # returns a new shortened list of lines.
984 fuzz = min(fuzz, len(old))
985 fuzz = min(fuzz, len(old))
985 if fuzz:
986 if fuzz:
986 top = 0
987 top = 0
987 bot = 0
988 bot = 0
988 hlen = len(self.hunk)
989 hlen = len(self.hunk)
989 for x in xrange(hlen - 1):
990 for x in xrange(hlen - 1):
990 # the hunk starts with the @@ line, so use x+1
991 # the hunk starts with the @@ line, so use x+1
991 if self.hunk[x + 1][0] == ' ':
992 if self.hunk[x + 1][0] == ' ':
992 top += 1
993 top += 1
993 else:
994 else:
994 break
995 break
995 if not toponly:
996 if not toponly:
996 for x in xrange(hlen - 1):
997 for x in xrange(hlen - 1):
997 if self.hunk[hlen - bot - 1][0] == ' ':
998 if self.hunk[hlen - bot - 1][0] == ' ':
998 bot += 1
999 bot += 1
999 else:
1000 else:
1000 break
1001 break
1001
1002
1002 bot = min(fuzz, bot)
1003 bot = min(fuzz, bot)
1003 top = min(fuzz, top)
1004 top = min(fuzz, top)
1004 return old[top:len(old) - bot], new[top:len(new) - bot], top
1005 return old[top:len(old) - bot], new[top:len(new) - bot], top
1005 return old, new, 0
1006 return old, new, 0
1006
1007
1007 def fuzzit(self, fuzz, toponly):
1008 def fuzzit(self, fuzz, toponly):
1008 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1009 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1009 oldstart = self.starta + top
1010 oldstart = self.starta + top
1010 newstart = self.startb + top
1011 newstart = self.startb + top
1011 # zero length hunk ranges already have their start decremented
1012 # zero length hunk ranges already have their start decremented
1012 if self.lena and oldstart > 0:
1013 if self.lena and oldstart > 0:
1013 oldstart -= 1
1014 oldstart -= 1
1014 if self.lenb and newstart > 0:
1015 if self.lenb and newstart > 0:
1015 newstart -= 1
1016 newstart -= 1
1016 return old, oldstart, new, newstart
1017 return old, oldstart, new, newstart
1017
1018
1018 class binhunk(object):
1019 class binhunk(object):
1019 'A binary patch file. Only understands literals so far.'
1020 'A binary patch file.'
1020 def __init__(self, lr, fname):
1021 def __init__(self, lr, fname):
1021 self.text = None
1022 self.text = None
1023 self.delta = False
1022 self.hunk = ['GIT binary patch\n']
1024 self.hunk = ['GIT binary patch\n']
1023 self._fname = fname
1025 self._fname = fname
1024 self._read(lr)
1026 self._read(lr)
1025
1027
1026 def complete(self):
1028 def complete(self):
1027 return self.text is not None
1029 return self.text is not None
1028
1030
1029 def new(self):
1031 def new(self, lines):
1032 if self.delta:
1033 return [applybindelta(self.text, ''.join(lines))]
1030 return [self.text]
1034 return [self.text]
1031
1035
1032 def _read(self, lr):
1036 def _read(self, lr):
1033 def getline(lr, hunk):
1037 def getline(lr, hunk):
1034 l = lr.readline()
1038 l = lr.readline()
1035 hunk.append(l)
1039 hunk.append(l)
1036 return l.rstrip('\r\n')
1040 return l.rstrip('\r\n')
1037
1041
1042 size = 0
1038 while True:
1043 while True:
1039 line = getline(lr, self.hunk)
1044 line = getline(lr, self.hunk)
1040 if not line:
1045 if not line:
1041 raise PatchError(_('could not extract "%s" binary data')
1046 raise PatchError(_('could not extract "%s" binary data')
1042 % self._fname)
1047 % self._fname)
1043 if line.startswith('literal '):
1048 if line.startswith('literal '):
1049 size = int(line[8:].rstrip())
1044 break
1050 break
1045 size = int(line[8:].rstrip())
1051 if line.startswith('delta '):
1052 size = int(line[6:].rstrip())
1053 self.delta = True
1054 break
1046 dec = []
1055 dec = []
1047 line = getline(lr, self.hunk)
1056 line = getline(lr, self.hunk)
1048 while len(line) > 1:
1057 while len(line) > 1:
1049 l = line[0]
1058 l = line[0]
1050 if l <= 'Z' and l >= 'A':
1059 if l <= 'Z' and l >= 'A':
1051 l = ord(l) - ord('A') + 1
1060 l = ord(l) - ord('A') + 1
1052 else:
1061 else:
1053 l = ord(l) - ord('a') + 27
1062 l = ord(l) - ord('a') + 27
1054 try:
1063 try:
1055 dec.append(base85.b85decode(line[1:])[:l])
1064 dec.append(base85.b85decode(line[1:])[:l])
1056 except ValueError, e:
1065 except ValueError, e:
1057 raise PatchError(_('could not decode "%s" binary patch: %s')
1066 raise PatchError(_('could not decode "%s" binary patch: %s')
1058 % (self._fname, str(e)))
1067 % (self._fname, str(e)))
1059 line = getline(lr, self.hunk)
1068 line = getline(lr, self.hunk)
1060 text = zlib.decompress(''.join(dec))
1069 text = zlib.decompress(''.join(dec))
1061 if len(text) != size:
1070 if len(text) != size:
1062 raise PatchError(_('"%s" length is %d bytes, should be %d')
1071 raise PatchError(_('"%s" length is %d bytes, should be %d')
1063 % (self._fname, len(text), size))
1072 % (self._fname, len(text), size))
1064 self.text = text
1073 self.text = text
1065
1074
1066 def parsefilename(str):
1075 def parsefilename(str):
1067 # --- filename \t|space stuff
1076 # --- filename \t|space stuff
1068 s = str[4:].rstrip('\r\n')
1077 s = str[4:].rstrip('\r\n')
1069 i = s.find('\t')
1078 i = s.find('\t')
1070 if i < 0:
1079 if i < 0:
1071 i = s.find(' ')
1080 i = s.find(' ')
1072 if i < 0:
1081 if i < 0:
1073 return s
1082 return s
1074 return s[:i]
1083 return s[:i]
1075
1084
1076 def pathstrip(path, strip):
1085 def pathstrip(path, strip):
1077 pathlen = len(path)
1086 pathlen = len(path)
1078 i = 0
1087 i = 0
1079 if strip == 0:
1088 if strip == 0:
1080 return '', path.rstrip()
1089 return '', path.rstrip()
1081 count = strip
1090 count = strip
1082 while count > 0:
1091 while count > 0:
1083 i = path.find('/', i)
1092 i = path.find('/', i)
1084 if i == -1:
1093 if i == -1:
1085 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1094 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1086 (count, strip, path))
1095 (count, strip, path))
1087 i += 1
1096 i += 1
1088 # consume '//' in the path
1097 # consume '//' in the path
1089 while i < pathlen - 1 and path[i] == '/':
1098 while i < pathlen - 1 and path[i] == '/':
1090 i += 1
1099 i += 1
1091 count -= 1
1100 count -= 1
1092 return path[:i].lstrip(), path[i:].rstrip()
1101 return path[:i].lstrip(), path[i:].rstrip()
1093
1102
1094 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip):
1103 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip):
1095 nulla = afile_orig == "/dev/null"
1104 nulla = afile_orig == "/dev/null"
1096 nullb = bfile_orig == "/dev/null"
1105 nullb = bfile_orig == "/dev/null"
1097 create = nulla and hunk.starta == 0 and hunk.lena == 0
1106 create = nulla and hunk.starta == 0 and hunk.lena == 0
1098 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1107 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1099 abase, afile = pathstrip(afile_orig, strip)
1108 abase, afile = pathstrip(afile_orig, strip)
1100 gooda = not nulla and backend.exists(afile)
1109 gooda = not nulla and backend.exists(afile)
1101 bbase, bfile = pathstrip(bfile_orig, strip)
1110 bbase, bfile = pathstrip(bfile_orig, strip)
1102 if afile == bfile:
1111 if afile == bfile:
1103 goodb = gooda
1112 goodb = gooda
1104 else:
1113 else:
1105 goodb = not nullb and backend.exists(bfile)
1114 goodb = not nullb and backend.exists(bfile)
1106 missing = not goodb and not gooda and not create
1115 missing = not goodb and not gooda and not create
1107
1116
1108 # some diff programs apparently produce patches where the afile is
1117 # some diff programs apparently produce patches where the afile is
1109 # not /dev/null, but afile starts with bfile
1118 # not /dev/null, but afile starts with bfile
1110 abasedir = afile[:afile.rfind('/') + 1]
1119 abasedir = afile[:afile.rfind('/') + 1]
1111 bbasedir = bfile[:bfile.rfind('/') + 1]
1120 bbasedir = bfile[:bfile.rfind('/') + 1]
1112 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1121 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1113 and hunk.starta == 0 and hunk.lena == 0):
1122 and hunk.starta == 0 and hunk.lena == 0):
1114 create = True
1123 create = True
1115 missing = False
1124 missing = False
1116
1125
1117 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1126 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1118 # diff is between a file and its backup. In this case, the original
1127 # diff is between a file and its backup. In this case, the original
1119 # file should be patched (see original mpatch code).
1128 # file should be patched (see original mpatch code).
1120 isbackup = (abase == bbase and bfile.startswith(afile))
1129 isbackup = (abase == bbase and bfile.startswith(afile))
1121 fname = None
1130 fname = None
1122 if not missing:
1131 if not missing:
1123 if gooda and goodb:
1132 if gooda and goodb:
1124 fname = isbackup and afile or bfile
1133 fname = isbackup and afile or bfile
1125 elif gooda:
1134 elif gooda:
1126 fname = afile
1135 fname = afile
1127
1136
1128 if not fname:
1137 if not fname:
1129 if not nullb:
1138 if not nullb:
1130 fname = isbackup and afile or bfile
1139 fname = isbackup and afile or bfile
1131 elif not nulla:
1140 elif not nulla:
1132 fname = afile
1141 fname = afile
1133 else:
1142 else:
1134 raise PatchError(_("undefined source and destination files"))
1143 raise PatchError(_("undefined source and destination files"))
1135
1144
1136 gp = patchmeta(fname)
1145 gp = patchmeta(fname)
1137 if create:
1146 if create:
1138 gp.op = 'ADD'
1147 gp.op = 'ADD'
1139 elif remove:
1148 elif remove:
1140 gp.op = 'DELETE'
1149 gp.op = 'DELETE'
1141 return gp
1150 return gp
1142
1151
1143 def scangitpatch(lr, firstline):
1152 def scangitpatch(lr, firstline):
1144 """
1153 """
1145 Git patches can emit:
1154 Git patches can emit:
1146 - rename a to b
1155 - rename a to b
1147 - change b
1156 - change b
1148 - copy a to c
1157 - copy a to c
1149 - change c
1158 - change c
1150
1159
1151 We cannot apply this sequence as-is, the renamed 'a' could not be
1160 We cannot apply this sequence as-is, the renamed 'a' could not be
1152 found for it would have been renamed already. And we cannot copy
1161 found for it would have been renamed already. And we cannot copy
1153 from 'b' instead because 'b' would have been changed already. So
1162 from 'b' instead because 'b' would have been changed already. So
1154 we scan the git patch for copy and rename commands so we can
1163 we scan the git patch for copy and rename commands so we can
1155 perform the copies ahead of time.
1164 perform the copies ahead of time.
1156 """
1165 """
1157 pos = 0
1166 pos = 0
1158 try:
1167 try:
1159 pos = lr.fp.tell()
1168 pos = lr.fp.tell()
1160 fp = lr.fp
1169 fp = lr.fp
1161 except IOError:
1170 except IOError:
1162 fp = cStringIO.StringIO(lr.fp.read())
1171 fp = cStringIO.StringIO(lr.fp.read())
1163 gitlr = linereader(fp)
1172 gitlr = linereader(fp)
1164 gitlr.push(firstline)
1173 gitlr.push(firstline)
1165 gitpatches = readgitpatch(gitlr)
1174 gitpatches = readgitpatch(gitlr)
1166 fp.seek(pos)
1175 fp.seek(pos)
1167 return gitpatches
1176 return gitpatches
1168
1177
1169 def iterhunks(fp):
1178 def iterhunks(fp):
1170 """Read a patch and yield the following events:
1179 """Read a patch and yield the following events:
1171 - ("file", afile, bfile, firsthunk): select a new target file.
1180 - ("file", afile, bfile, firsthunk): select a new target file.
1172 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1181 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1173 "file" event.
1182 "file" event.
1174 - ("git", gitchanges): current diff is in git format, gitchanges
1183 - ("git", gitchanges): current diff is in git format, gitchanges
1175 maps filenames to gitpatch records. Unique event.
1184 maps filenames to gitpatch records. Unique event.
1176 """
1185 """
1177 afile = ""
1186 afile = ""
1178 bfile = ""
1187 bfile = ""
1179 state = None
1188 state = None
1180 hunknum = 0
1189 hunknum = 0
1181 emitfile = newfile = False
1190 emitfile = newfile = False
1182 gitpatches = None
1191 gitpatches = None
1183
1192
1184 # our states
1193 # our states
1185 BFILE = 1
1194 BFILE = 1
1186 context = None
1195 context = None
1187 lr = linereader(fp)
1196 lr = linereader(fp)
1188
1197
1189 while True:
1198 while True:
1190 x = lr.readline()
1199 x = lr.readline()
1191 if not x:
1200 if not x:
1192 break
1201 break
1193 if state == BFILE and (
1202 if state == BFILE and (
1194 (not context and x[0] == '@')
1203 (not context and x[0] == '@')
1195 or (context is not False and x.startswith('***************'))
1204 or (context is not False and x.startswith('***************'))
1196 or x.startswith('GIT binary patch')):
1205 or x.startswith('GIT binary patch')):
1197 gp = None
1206 gp = None
1198 if (gitpatches and
1207 if (gitpatches and
1199 gitpatches[-1].ispatching(afile, bfile)):
1208 gitpatches[-1].ispatching(afile, bfile)):
1200 gp = gitpatches.pop()
1209 gp = gitpatches.pop()
1201 if x.startswith('GIT binary patch'):
1210 if x.startswith('GIT binary patch'):
1202 h = binhunk(lr, gp.path)
1211 h = binhunk(lr, gp.path)
1203 else:
1212 else:
1204 if context is None and x.startswith('***************'):
1213 if context is None and x.startswith('***************'):
1205 context = True
1214 context = True
1206 h = hunk(x, hunknum + 1, lr, context)
1215 h = hunk(x, hunknum + 1, lr, context)
1207 hunknum += 1
1216 hunknum += 1
1208 if emitfile:
1217 if emitfile:
1209 emitfile = False
1218 emitfile = False
1210 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1219 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1211 yield 'hunk', h
1220 yield 'hunk', h
1212 elif x.startswith('diff --git a/'):
1221 elif x.startswith('diff --git a/'):
1213 m = gitre.match(x.rstrip(' \r\n'))
1222 m = gitre.match(x.rstrip(' \r\n'))
1214 if not m:
1223 if not m:
1215 continue
1224 continue
1216 if gitpatches is None:
1225 if gitpatches is None:
1217 # scan whole input for git metadata
1226 # scan whole input for git metadata
1218 gitpatches = scangitpatch(lr, x)
1227 gitpatches = scangitpatch(lr, x)
1219 yield 'git', [g.copy() for g in gitpatches
1228 yield 'git', [g.copy() for g in gitpatches
1220 if g.op in ('COPY', 'RENAME')]
1229 if g.op in ('COPY', 'RENAME')]
1221 gitpatches.reverse()
1230 gitpatches.reverse()
1222 afile = 'a/' + m.group(1)
1231 afile = 'a/' + m.group(1)
1223 bfile = 'b/' + m.group(2)
1232 bfile = 'b/' + m.group(2)
1224 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1233 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1225 gp = gitpatches.pop()
1234 gp = gitpatches.pop()
1226 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1235 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1227 if not gitpatches:
1236 if not gitpatches:
1228 raise PatchError(_('failed to synchronize metadata for "%s"')
1237 raise PatchError(_('failed to synchronize metadata for "%s"')
1229 % afile[2:])
1238 % afile[2:])
1230 gp = gitpatches[-1]
1239 gp = gitpatches[-1]
1231 newfile = True
1240 newfile = True
1232 elif x.startswith('---'):
1241 elif x.startswith('---'):
1233 # check for a unified diff
1242 # check for a unified diff
1234 l2 = lr.readline()
1243 l2 = lr.readline()
1235 if not l2.startswith('+++'):
1244 if not l2.startswith('+++'):
1236 lr.push(l2)
1245 lr.push(l2)
1237 continue
1246 continue
1238 newfile = True
1247 newfile = True
1239 context = False
1248 context = False
1240 afile = parsefilename(x)
1249 afile = parsefilename(x)
1241 bfile = parsefilename(l2)
1250 bfile = parsefilename(l2)
1242 elif x.startswith('***'):
1251 elif x.startswith('***'):
1243 # check for a context diff
1252 # check for a context diff
1244 l2 = lr.readline()
1253 l2 = lr.readline()
1245 if not l2.startswith('---'):
1254 if not l2.startswith('---'):
1246 lr.push(l2)
1255 lr.push(l2)
1247 continue
1256 continue
1248 l3 = lr.readline()
1257 l3 = lr.readline()
1249 lr.push(l3)
1258 lr.push(l3)
1250 if not l3.startswith("***************"):
1259 if not l3.startswith("***************"):
1251 lr.push(l2)
1260 lr.push(l2)
1252 continue
1261 continue
1253 newfile = True
1262 newfile = True
1254 context = True
1263 context = True
1255 afile = parsefilename(x)
1264 afile = parsefilename(x)
1256 bfile = parsefilename(l2)
1265 bfile = parsefilename(l2)
1257
1266
1258 if newfile:
1267 if newfile:
1259 newfile = False
1268 newfile = False
1260 emitfile = True
1269 emitfile = True
1261 state = BFILE
1270 state = BFILE
1262 hunknum = 0
1271 hunknum = 0
1263
1272
1264 while gitpatches:
1273 while gitpatches:
1265 gp = gitpatches.pop()
1274 gp = gitpatches.pop()
1266 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1275 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1267
1276
1277 def applybindelta(binchunk, data):
1278 """Apply a binary delta hunk
1279 The algorithm used is the algorithm from git's patch-delta.c
1280 """
1281 def deltahead(binchunk):
1282 i = 0
1283 for c in binchunk:
1284 i += 1
1285 if not (ord(c) & 0x80):
1286 return i
1287 return i
1288 out = ""
1289 s = deltahead(binchunk)
1290 binchunk = binchunk[s:]
1291 s = deltahead(binchunk)
1292 binchunk = binchunk[s:]
1293 i = 0
1294 while i < len(binchunk):
1295 cmd = ord(binchunk[i])
1296 i += 1
1297 if (cmd & 0x80):
1298 offset = 0
1299 size = 0
1300 if (cmd & 0x01):
1301 offset = ord(binchunk[i])
1302 i += 1
1303 if (cmd & 0x02):
1304 offset |= ord(binchunk[i]) << 8
1305 i += 1
1306 if (cmd & 0x04):
1307 offset |= ord(binchunk[i]) << 16
1308 i += 1
1309 if (cmd & 0x08):
1310 offset |= ord(binchunk[i]) << 24
1311 i += 1
1312 if (cmd & 0x10):
1313 size = ord(binchunk[i])
1314 i += 1
1315 if (cmd & 0x20):
1316 size |= ord(binchunk[i]) << 8
1317 i += 1
1318 if (cmd & 0x40):
1319 size |= ord(binchunk[i]) << 16
1320 i += 1
1321 if size == 0:
1322 size = 0x10000
1323 offset_end = offset + size
1324 out += data[offset:offset_end]
1325 elif cmd != 0:
1326 offset_end = i + cmd
1327 out += binchunk[i:offset_end]
1328 i += cmd
1329 else:
1330 raise PatchError(_('unexpected delta opcode 0'))
1331 return out
1332
1268 def applydiff(ui, fp, backend, store, strip=1, eolmode='strict'):
1333 def applydiff(ui, fp, backend, store, strip=1, eolmode='strict'):
1269 """Reads a patch from fp and tries to apply it.
1334 """Reads a patch from fp and tries to apply it.
1270
1335
1271 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1336 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1272 there was any fuzz.
1337 there was any fuzz.
1273
1338
1274 If 'eolmode' is 'strict', the patch content and patched file are
1339 If 'eolmode' is 'strict', the patch content and patched file are
1275 read in binary mode. Otherwise, line endings are ignored when
1340 read in binary mode. Otherwise, line endings are ignored when
1276 patching then normalized according to 'eolmode'.
1341 patching then normalized according to 'eolmode'.
1277 """
1342 """
1278 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1343 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1279 eolmode=eolmode)
1344 eolmode=eolmode)
1280
1345
1281 def _applydiff(ui, fp, patcher, backend, store, strip=1,
1346 def _applydiff(ui, fp, patcher, backend, store, strip=1,
1282 eolmode='strict'):
1347 eolmode='strict'):
1283
1348
1284 def pstrip(p):
1349 def pstrip(p):
1285 return pathstrip(p, strip - 1)[1]
1350 return pathstrip(p, strip - 1)[1]
1286
1351
1287 rejects = 0
1352 rejects = 0
1288 err = 0
1353 err = 0
1289 current_file = None
1354 current_file = None
1290
1355
1291 for state, values in iterhunks(fp):
1356 for state, values in iterhunks(fp):
1292 if state == 'hunk':
1357 if state == 'hunk':
1293 if not current_file:
1358 if not current_file:
1294 continue
1359 continue
1295 ret = current_file.apply(values)
1360 ret = current_file.apply(values)
1296 if ret > 0:
1361 if ret > 0:
1297 err = 1
1362 err = 1
1298 elif state == 'file':
1363 elif state == 'file':
1299 if current_file:
1364 if current_file:
1300 rejects += current_file.close()
1365 rejects += current_file.close()
1301 current_file = None
1366 current_file = None
1302 afile, bfile, first_hunk, gp = values
1367 afile, bfile, first_hunk, gp = values
1303 if gp:
1368 if gp:
1304 gp.path = pstrip(gp.path)
1369 gp.path = pstrip(gp.path)
1305 if gp.oldpath:
1370 if gp.oldpath:
1306 gp.oldpath = pstrip(gp.oldpath)
1371 gp.oldpath = pstrip(gp.oldpath)
1307 else:
1372 else:
1308 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1373 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1309 if gp.op == 'RENAME':
1374 if gp.op == 'RENAME':
1310 backend.unlink(gp.oldpath)
1375 backend.unlink(gp.oldpath)
1311 if not first_hunk:
1376 if not first_hunk:
1312 if gp.op == 'DELETE':
1377 if gp.op == 'DELETE':
1313 backend.unlink(gp.path)
1378 backend.unlink(gp.path)
1314 continue
1379 continue
1315 data, mode = None, None
1380 data, mode = None, None
1316 if gp.op in ('RENAME', 'COPY'):
1381 if gp.op in ('RENAME', 'COPY'):
1317 data, mode = store.getfile(gp.oldpath)[:2]
1382 data, mode = store.getfile(gp.oldpath)[:2]
1318 if gp.mode:
1383 if gp.mode:
1319 mode = gp.mode
1384 mode = gp.mode
1320 if gp.op == 'ADD':
1385 if gp.op == 'ADD':
1321 # Added files without content have no hunk and
1386 # Added files without content have no hunk and
1322 # must be created
1387 # must be created
1323 data = ''
1388 data = ''
1324 if data or mode:
1389 if data or mode:
1325 if (gp.op in ('ADD', 'RENAME', 'COPY')
1390 if (gp.op in ('ADD', 'RENAME', 'COPY')
1326 and backend.exists(gp.path)):
1391 and backend.exists(gp.path)):
1327 raise PatchError(_("cannot create %s: destination "
1392 raise PatchError(_("cannot create %s: destination "
1328 "already exists") % gp.path)
1393 "already exists") % gp.path)
1329 backend.setfile(gp.path, data, mode, gp.oldpath)
1394 backend.setfile(gp.path, data, mode, gp.oldpath)
1330 continue
1395 continue
1331 try:
1396 try:
1332 current_file = patcher(ui, gp, backend, store,
1397 current_file = patcher(ui, gp, backend, store,
1333 eolmode=eolmode)
1398 eolmode=eolmode)
1334 except PatchError, inst:
1399 except PatchError, inst:
1335 ui.warn(str(inst) + '\n')
1400 ui.warn(str(inst) + '\n')
1336 current_file = None
1401 current_file = None
1337 rejects += 1
1402 rejects += 1
1338 continue
1403 continue
1339 elif state == 'git':
1404 elif state == 'git':
1340 for gp in values:
1405 for gp in values:
1341 path = pstrip(gp.oldpath)
1406 path = pstrip(gp.oldpath)
1342 try:
1407 try:
1343 data, mode = backend.getfile(path)
1408 data, mode = backend.getfile(path)
1344 except IOError, e:
1409 except IOError, e:
1345 if e.errno != errno.ENOENT:
1410 if e.errno != errno.ENOENT:
1346 raise
1411 raise
1347 # The error ignored here will trigger a getfile()
1412 # The error ignored here will trigger a getfile()
1348 # error in a place more appropriate for error
1413 # error in a place more appropriate for error
1349 # handling, and will not interrupt the patching
1414 # handling, and will not interrupt the patching
1350 # process.
1415 # process.
1351 else:
1416 else:
1352 store.setfile(path, data, mode)
1417 store.setfile(path, data, mode)
1353 else:
1418 else:
1354 raise util.Abort(_('unsupported parser state: %s') % state)
1419 raise util.Abort(_('unsupported parser state: %s') % state)
1355
1420
1356 if current_file:
1421 if current_file:
1357 rejects += current_file.close()
1422 rejects += current_file.close()
1358
1423
1359 if rejects:
1424 if rejects:
1360 return -1
1425 return -1
1361 return err
1426 return err
1362
1427
1363 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1428 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1364 similarity):
1429 similarity):
1365 """use <patcher> to apply <patchname> to the working directory.
1430 """use <patcher> to apply <patchname> to the working directory.
1366 returns whether patch was applied with fuzz factor."""
1431 returns whether patch was applied with fuzz factor."""
1367
1432
1368 fuzz = False
1433 fuzz = False
1369 args = []
1434 args = []
1370 cwd = repo.root
1435 cwd = repo.root
1371 if cwd:
1436 if cwd:
1372 args.append('-d %s' % util.shellquote(cwd))
1437 args.append('-d %s' % util.shellquote(cwd))
1373 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1438 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1374 util.shellquote(patchname)))
1439 util.shellquote(patchname)))
1375 try:
1440 try:
1376 for line in fp:
1441 for line in fp:
1377 line = line.rstrip()
1442 line = line.rstrip()
1378 ui.note(line + '\n')
1443 ui.note(line + '\n')
1379 if line.startswith('patching file '):
1444 if line.startswith('patching file '):
1380 pf = util.parsepatchoutput(line)
1445 pf = util.parsepatchoutput(line)
1381 printed_file = False
1446 printed_file = False
1382 files.add(pf)
1447 files.add(pf)
1383 elif line.find('with fuzz') >= 0:
1448 elif line.find('with fuzz') >= 0:
1384 fuzz = True
1449 fuzz = True
1385 if not printed_file:
1450 if not printed_file:
1386 ui.warn(pf + '\n')
1451 ui.warn(pf + '\n')
1387 printed_file = True
1452 printed_file = True
1388 ui.warn(line + '\n')
1453 ui.warn(line + '\n')
1389 elif line.find('saving rejects to file') >= 0:
1454 elif line.find('saving rejects to file') >= 0:
1390 ui.warn(line + '\n')
1455 ui.warn(line + '\n')
1391 elif line.find('FAILED') >= 0:
1456 elif line.find('FAILED') >= 0:
1392 if not printed_file:
1457 if not printed_file:
1393 ui.warn(pf + '\n')
1458 ui.warn(pf + '\n')
1394 printed_file = True
1459 printed_file = True
1395 ui.warn(line + '\n')
1460 ui.warn(line + '\n')
1396 finally:
1461 finally:
1397 if files:
1462 if files:
1398 scmutil.marktouched(repo, files, similarity)
1463 scmutil.marktouched(repo, files, similarity)
1399 code = fp.close()
1464 code = fp.close()
1400 if code:
1465 if code:
1401 raise PatchError(_("patch command failed: %s") %
1466 raise PatchError(_("patch command failed: %s") %
1402 util.explainexit(code)[0])
1467 util.explainexit(code)[0])
1403 return fuzz
1468 return fuzz
1404
1469
1405 def patchbackend(ui, backend, patchobj, strip, files=None, eolmode='strict'):
1470 def patchbackend(ui, backend, patchobj, strip, files=None, eolmode='strict'):
1406 if files is None:
1471 if files is None:
1407 files = set()
1472 files = set()
1408 if eolmode is None:
1473 if eolmode is None:
1409 eolmode = ui.config('patch', 'eol', 'strict')
1474 eolmode = ui.config('patch', 'eol', 'strict')
1410 if eolmode.lower() not in eolmodes:
1475 if eolmode.lower() not in eolmodes:
1411 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1476 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1412 eolmode = eolmode.lower()
1477 eolmode = eolmode.lower()
1413
1478
1414 store = filestore()
1479 store = filestore()
1415 try:
1480 try:
1416 fp = open(patchobj, 'rb')
1481 fp = open(patchobj, 'rb')
1417 except TypeError:
1482 except TypeError:
1418 fp = patchobj
1483 fp = patchobj
1419 try:
1484 try:
1420 ret = applydiff(ui, fp, backend, store, strip=strip,
1485 ret = applydiff(ui, fp, backend, store, strip=strip,
1421 eolmode=eolmode)
1486 eolmode=eolmode)
1422 finally:
1487 finally:
1423 if fp != patchobj:
1488 if fp != patchobj:
1424 fp.close()
1489 fp.close()
1425 files.update(backend.close())
1490 files.update(backend.close())
1426 store.close()
1491 store.close()
1427 if ret < 0:
1492 if ret < 0:
1428 raise PatchError(_('patch failed to apply'))
1493 raise PatchError(_('patch failed to apply'))
1429 return ret > 0
1494 return ret > 0
1430
1495
1431 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1496 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1432 similarity=0):
1497 similarity=0):
1433 """use builtin patch to apply <patchobj> to the working directory.
1498 """use builtin patch to apply <patchobj> to the working directory.
1434 returns whether patch was applied with fuzz factor."""
1499 returns whether patch was applied with fuzz factor."""
1435 backend = workingbackend(ui, repo, similarity)
1500 backend = workingbackend(ui, repo, similarity)
1436 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1501 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1437
1502
1438 def patchrepo(ui, repo, ctx, store, patchobj, strip, files=None,
1503 def patchrepo(ui, repo, ctx, store, patchobj, strip, files=None,
1439 eolmode='strict'):
1504 eolmode='strict'):
1440 backend = repobackend(ui, repo, ctx, store)
1505 backend = repobackend(ui, repo, ctx, store)
1441 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1506 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1442
1507
1443 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1508 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1444 similarity=0):
1509 similarity=0):
1445 """Apply <patchname> to the working directory.
1510 """Apply <patchname> to the working directory.
1446
1511
1447 'eolmode' specifies how end of lines should be handled. It can be:
1512 'eolmode' specifies how end of lines should be handled. It can be:
1448 - 'strict': inputs are read in binary mode, EOLs are preserved
1513 - 'strict': inputs are read in binary mode, EOLs are preserved
1449 - 'crlf': EOLs are ignored when patching and reset to CRLF
1514 - 'crlf': EOLs are ignored when patching and reset to CRLF
1450 - 'lf': EOLs are ignored when patching and reset to LF
1515 - 'lf': EOLs are ignored when patching and reset to LF
1451 - None: get it from user settings, default to 'strict'
1516 - None: get it from user settings, default to 'strict'
1452 'eolmode' is ignored when using an external patcher program.
1517 'eolmode' is ignored when using an external patcher program.
1453
1518
1454 Returns whether patch was applied with fuzz factor.
1519 Returns whether patch was applied with fuzz factor.
1455 """
1520 """
1456 patcher = ui.config('ui', 'patch')
1521 patcher = ui.config('ui', 'patch')
1457 if files is None:
1522 if files is None:
1458 files = set()
1523 files = set()
1459 try:
1524 try:
1460 if patcher:
1525 if patcher:
1461 return _externalpatch(ui, repo, patcher, patchname, strip,
1526 return _externalpatch(ui, repo, patcher, patchname, strip,
1462 files, similarity)
1527 files, similarity)
1463 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1528 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1464 similarity)
1529 similarity)
1465 except PatchError, err:
1530 except PatchError, err:
1466 raise util.Abort(str(err))
1531 raise util.Abort(str(err))
1467
1532
1468 def changedfiles(ui, repo, patchpath, strip=1):
1533 def changedfiles(ui, repo, patchpath, strip=1):
1469 backend = fsbackend(ui, repo.root)
1534 backend = fsbackend(ui, repo.root)
1470 fp = open(patchpath, 'rb')
1535 fp = open(patchpath, 'rb')
1471 try:
1536 try:
1472 changed = set()
1537 changed = set()
1473 for state, values in iterhunks(fp):
1538 for state, values in iterhunks(fp):
1474 if state == 'file':
1539 if state == 'file':
1475 afile, bfile, first_hunk, gp = values
1540 afile, bfile, first_hunk, gp = values
1476 if gp:
1541 if gp:
1477 gp.path = pathstrip(gp.path, strip - 1)[1]
1542 gp.path = pathstrip(gp.path, strip - 1)[1]
1478 if gp.oldpath:
1543 if gp.oldpath:
1479 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1544 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1480 else:
1545 else:
1481 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1546 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1482 changed.add(gp.path)
1547 changed.add(gp.path)
1483 if gp.op == 'RENAME':
1548 if gp.op == 'RENAME':
1484 changed.add(gp.oldpath)
1549 changed.add(gp.oldpath)
1485 elif state not in ('hunk', 'git'):
1550 elif state not in ('hunk', 'git'):
1486 raise util.Abort(_('unsupported parser state: %s') % state)
1551 raise util.Abort(_('unsupported parser state: %s') % state)
1487 return changed
1552 return changed
1488 finally:
1553 finally:
1489 fp.close()
1554 fp.close()
1490
1555
1491 class GitDiffRequired(Exception):
1556 class GitDiffRequired(Exception):
1492 pass
1557 pass
1493
1558
1494 def diffopts(ui, opts=None, untrusted=False, section='diff'):
1559 def diffopts(ui, opts=None, untrusted=False, section='diff'):
1495 def get(key, name=None, getter=ui.configbool):
1560 def get(key, name=None, getter=ui.configbool):
1496 return ((opts and opts.get(key)) or
1561 return ((opts and opts.get(key)) or
1497 getter(section, name or key, None, untrusted=untrusted))
1562 getter(section, name or key, None, untrusted=untrusted))
1498 return mdiff.diffopts(
1563 return mdiff.diffopts(
1499 text=opts and opts.get('text'),
1564 text=opts and opts.get('text'),
1500 git=get('git'),
1565 git=get('git'),
1501 nodates=get('nodates'),
1566 nodates=get('nodates'),
1502 showfunc=get('show_function', 'showfunc'),
1567 showfunc=get('show_function', 'showfunc'),
1503 ignorews=get('ignore_all_space', 'ignorews'),
1568 ignorews=get('ignore_all_space', 'ignorews'),
1504 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1569 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1505 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1570 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1506 context=get('unified', getter=ui.config))
1571 context=get('unified', getter=ui.config))
1507
1572
1508 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1573 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1509 losedatafn=None, prefix=''):
1574 losedatafn=None, prefix=''):
1510 '''yields diff of changes to files between two nodes, or node and
1575 '''yields diff of changes to files between two nodes, or node and
1511 working directory.
1576 working directory.
1512
1577
1513 if node1 is None, use first dirstate parent instead.
1578 if node1 is None, use first dirstate parent instead.
1514 if node2 is None, compare node1 with working directory.
1579 if node2 is None, compare node1 with working directory.
1515
1580
1516 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1581 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1517 every time some change cannot be represented with the current
1582 every time some change cannot be represented with the current
1518 patch format. Return False to upgrade to git patch format, True to
1583 patch format. Return False to upgrade to git patch format, True to
1519 accept the loss or raise an exception to abort the diff. It is
1584 accept the loss or raise an exception to abort the diff. It is
1520 called with the name of current file being diffed as 'fn'. If set
1585 called with the name of current file being diffed as 'fn'. If set
1521 to None, patches will always be upgraded to git format when
1586 to None, patches will always be upgraded to git format when
1522 necessary.
1587 necessary.
1523
1588
1524 prefix is a filename prefix that is prepended to all filenames on
1589 prefix is a filename prefix that is prepended to all filenames on
1525 display (used for subrepos).
1590 display (used for subrepos).
1526 '''
1591 '''
1527
1592
1528 if opts is None:
1593 if opts is None:
1529 opts = mdiff.defaultopts
1594 opts = mdiff.defaultopts
1530
1595
1531 if not node1 and not node2:
1596 if not node1 and not node2:
1532 node1 = repo.dirstate.p1()
1597 node1 = repo.dirstate.p1()
1533
1598
1534 def lrugetfilectx():
1599 def lrugetfilectx():
1535 cache = {}
1600 cache = {}
1536 order = util.deque()
1601 order = util.deque()
1537 def getfilectx(f, ctx):
1602 def getfilectx(f, ctx):
1538 fctx = ctx.filectx(f, filelog=cache.get(f))
1603 fctx = ctx.filectx(f, filelog=cache.get(f))
1539 if f not in cache:
1604 if f not in cache:
1540 if len(cache) > 20:
1605 if len(cache) > 20:
1541 del cache[order.popleft()]
1606 del cache[order.popleft()]
1542 cache[f] = fctx.filelog()
1607 cache[f] = fctx.filelog()
1543 else:
1608 else:
1544 order.remove(f)
1609 order.remove(f)
1545 order.append(f)
1610 order.append(f)
1546 return fctx
1611 return fctx
1547 return getfilectx
1612 return getfilectx
1548 getfilectx = lrugetfilectx()
1613 getfilectx = lrugetfilectx()
1549
1614
1550 ctx1 = repo[node1]
1615 ctx1 = repo[node1]
1551 ctx2 = repo[node2]
1616 ctx2 = repo[node2]
1552
1617
1553 if not changes:
1618 if not changes:
1554 changes = repo.status(ctx1, ctx2, match=match)
1619 changes = repo.status(ctx1, ctx2, match=match)
1555 modified, added, removed = changes[:3]
1620 modified, added, removed = changes[:3]
1556
1621
1557 if not modified and not added and not removed:
1622 if not modified and not added and not removed:
1558 return []
1623 return []
1559
1624
1560 revs = None
1625 revs = None
1561 hexfunc = repo.ui.debugflag and hex or short
1626 hexfunc = repo.ui.debugflag and hex or short
1562 revs = [hexfunc(node) for node in [node1, node2] if node]
1627 revs = [hexfunc(node) for node in [node1, node2] if node]
1563
1628
1564 copy = {}
1629 copy = {}
1565 if opts.git or opts.upgrade:
1630 if opts.git or opts.upgrade:
1566 copy = copies.pathcopies(ctx1, ctx2)
1631 copy = copies.pathcopies(ctx1, ctx2)
1567
1632
1568 def difffn(opts, losedata):
1633 def difffn(opts, losedata):
1569 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1634 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1570 copy, getfilectx, opts, losedata, prefix)
1635 copy, getfilectx, opts, losedata, prefix)
1571 if opts.upgrade and not opts.git:
1636 if opts.upgrade and not opts.git:
1572 try:
1637 try:
1573 def losedata(fn):
1638 def losedata(fn):
1574 if not losedatafn or not losedatafn(fn=fn):
1639 if not losedatafn or not losedatafn(fn=fn):
1575 raise GitDiffRequired
1640 raise GitDiffRequired
1576 # Buffer the whole output until we are sure it can be generated
1641 # Buffer the whole output until we are sure it can be generated
1577 return list(difffn(opts.copy(git=False), losedata))
1642 return list(difffn(opts.copy(git=False), losedata))
1578 except GitDiffRequired:
1643 except GitDiffRequired:
1579 return difffn(opts.copy(git=True), None)
1644 return difffn(opts.copy(git=True), None)
1580 else:
1645 else:
1581 return difffn(opts, None)
1646 return difffn(opts, None)
1582
1647
1583 def difflabel(func, *args, **kw):
1648 def difflabel(func, *args, **kw):
1584 '''yields 2-tuples of (output, label) based on the output of func()'''
1649 '''yields 2-tuples of (output, label) based on the output of func()'''
1585 headprefixes = [('diff', 'diff.diffline'),
1650 headprefixes = [('diff', 'diff.diffline'),
1586 ('copy', 'diff.extended'),
1651 ('copy', 'diff.extended'),
1587 ('rename', 'diff.extended'),
1652 ('rename', 'diff.extended'),
1588 ('old', 'diff.extended'),
1653 ('old', 'diff.extended'),
1589 ('new', 'diff.extended'),
1654 ('new', 'diff.extended'),
1590 ('deleted', 'diff.extended'),
1655 ('deleted', 'diff.extended'),
1591 ('---', 'diff.file_a'),
1656 ('---', 'diff.file_a'),
1592 ('+++', 'diff.file_b')]
1657 ('+++', 'diff.file_b')]
1593 textprefixes = [('@', 'diff.hunk'),
1658 textprefixes = [('@', 'diff.hunk'),
1594 ('-', 'diff.deleted'),
1659 ('-', 'diff.deleted'),
1595 ('+', 'diff.inserted')]
1660 ('+', 'diff.inserted')]
1596 head = False
1661 head = False
1597 for chunk in func(*args, **kw):
1662 for chunk in func(*args, **kw):
1598 lines = chunk.split('\n')
1663 lines = chunk.split('\n')
1599 for i, line in enumerate(lines):
1664 for i, line in enumerate(lines):
1600 if i != 0:
1665 if i != 0:
1601 yield ('\n', '')
1666 yield ('\n', '')
1602 if head:
1667 if head:
1603 if line.startswith('@'):
1668 if line.startswith('@'):
1604 head = False
1669 head = False
1605 else:
1670 else:
1606 if line and line[0] not in ' +-@\\':
1671 if line and line[0] not in ' +-@\\':
1607 head = True
1672 head = True
1608 stripline = line
1673 stripline = line
1609 if not head and line and line[0] in '+-':
1674 if not head and line and line[0] in '+-':
1610 # highlight trailing whitespace, but only in changed lines
1675 # highlight trailing whitespace, but only in changed lines
1611 stripline = line.rstrip()
1676 stripline = line.rstrip()
1612 prefixes = textprefixes
1677 prefixes = textprefixes
1613 if head:
1678 if head:
1614 prefixes = headprefixes
1679 prefixes = headprefixes
1615 for prefix, label in prefixes:
1680 for prefix, label in prefixes:
1616 if stripline.startswith(prefix):
1681 if stripline.startswith(prefix):
1617 yield (stripline, label)
1682 yield (stripline, label)
1618 break
1683 break
1619 else:
1684 else:
1620 yield (line, '')
1685 yield (line, '')
1621 if line != stripline:
1686 if line != stripline:
1622 yield (line[len(stripline):], 'diff.trailingwhitespace')
1687 yield (line[len(stripline):], 'diff.trailingwhitespace')
1623
1688
1624 def diffui(*args, **kw):
1689 def diffui(*args, **kw):
1625 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1690 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1626 return difflabel(diff, *args, **kw)
1691 return difflabel(diff, *args, **kw)
1627
1692
1628 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1693 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1629 copy, getfilectx, opts, losedatafn, prefix):
1694 copy, getfilectx, opts, losedatafn, prefix):
1630
1695
1631 def join(f):
1696 def join(f):
1632 return posixpath.join(prefix, f)
1697 return posixpath.join(prefix, f)
1633
1698
1634 def addmodehdr(header, omode, nmode):
1699 def addmodehdr(header, omode, nmode):
1635 if omode != nmode:
1700 if omode != nmode:
1636 header.append('old mode %s\n' % omode)
1701 header.append('old mode %s\n' % omode)
1637 header.append('new mode %s\n' % nmode)
1702 header.append('new mode %s\n' % nmode)
1638
1703
1639 def addindexmeta(meta, revs):
1704 def addindexmeta(meta, revs):
1640 if opts.git:
1705 if opts.git:
1641 i = len(revs)
1706 i = len(revs)
1642 if i==2:
1707 if i==2:
1643 meta.append('index %s..%s\n' % tuple(revs))
1708 meta.append('index %s..%s\n' % tuple(revs))
1644 elif i==3:
1709 elif i==3:
1645 meta.append('index %s,%s..%s\n' % tuple(revs))
1710 meta.append('index %s,%s..%s\n' % tuple(revs))
1646
1711
1647 def gitindex(text):
1712 def gitindex(text):
1648 if not text:
1713 if not text:
1649 text = ""
1714 text = ""
1650 l = len(text)
1715 l = len(text)
1651 s = util.sha1('blob %d\0' % l)
1716 s = util.sha1('blob %d\0' % l)
1652 s.update(text)
1717 s.update(text)
1653 return s.hexdigest()
1718 return s.hexdigest()
1654
1719
1655 def diffline(a, b, revs):
1720 def diffline(a, b, revs):
1656 if opts.git:
1721 if opts.git:
1657 line = 'diff --git a/%s b/%s\n' % (a, b)
1722 line = 'diff --git a/%s b/%s\n' % (a, b)
1658 elif not repo.ui.quiet:
1723 elif not repo.ui.quiet:
1659 if revs:
1724 if revs:
1660 revinfo = ' '.join(["-r %s" % rev for rev in revs])
1725 revinfo = ' '.join(["-r %s" % rev for rev in revs])
1661 line = 'diff %s %s\n' % (revinfo, a)
1726 line = 'diff %s %s\n' % (revinfo, a)
1662 else:
1727 else:
1663 line = 'diff %s\n' % a
1728 line = 'diff %s\n' % a
1664 else:
1729 else:
1665 line = ''
1730 line = ''
1666 return line
1731 return line
1667
1732
1668 date1 = util.datestr(ctx1.date())
1733 date1 = util.datestr(ctx1.date())
1669 man1 = ctx1.manifest()
1734 man1 = ctx1.manifest()
1670
1735
1671 gone = set()
1736 gone = set()
1672 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1737 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1673
1738
1674 copyto = dict([(v, k) for k, v in copy.items()])
1739 copyto = dict([(v, k) for k, v in copy.items()])
1675
1740
1676 if opts.git:
1741 if opts.git:
1677 revs = None
1742 revs = None
1678
1743
1679 for f in sorted(modified + added + removed):
1744 for f in sorted(modified + added + removed):
1680 to = None
1745 to = None
1681 tn = None
1746 tn = None
1682 dodiff = True
1747 dodiff = True
1683 header = []
1748 header = []
1684 if f in man1:
1749 if f in man1:
1685 to = getfilectx(f, ctx1).data()
1750 to = getfilectx(f, ctx1).data()
1686 if f not in removed:
1751 if f not in removed:
1687 tn = getfilectx(f, ctx2).data()
1752 tn = getfilectx(f, ctx2).data()
1688 a, b = f, f
1753 a, b = f, f
1689 if opts.git or losedatafn:
1754 if opts.git or losedatafn:
1690 if f in added or (f in modified and to is None):
1755 if f in added or (f in modified and to is None):
1691 mode = gitmode[ctx2.flags(f)]
1756 mode = gitmode[ctx2.flags(f)]
1692 if f in copy or f in copyto:
1757 if f in copy or f in copyto:
1693 if opts.git:
1758 if opts.git:
1694 if f in copy:
1759 if f in copy:
1695 a = copy[f]
1760 a = copy[f]
1696 else:
1761 else:
1697 a = copyto[f]
1762 a = copyto[f]
1698 omode = gitmode[man1.flags(a)]
1763 omode = gitmode[man1.flags(a)]
1699 addmodehdr(header, omode, mode)
1764 addmodehdr(header, omode, mode)
1700 if a in removed and a not in gone:
1765 if a in removed and a not in gone:
1701 op = 'rename'
1766 op = 'rename'
1702 gone.add(a)
1767 gone.add(a)
1703 else:
1768 else:
1704 op = 'copy'
1769 op = 'copy'
1705 header.append('%s from %s\n' % (op, join(a)))
1770 header.append('%s from %s\n' % (op, join(a)))
1706 header.append('%s to %s\n' % (op, join(f)))
1771 header.append('%s to %s\n' % (op, join(f)))
1707 to = getfilectx(a, ctx1).data()
1772 to = getfilectx(a, ctx1).data()
1708 else:
1773 else:
1709 losedatafn(f)
1774 losedatafn(f)
1710 else:
1775 else:
1711 if opts.git:
1776 if opts.git:
1712 header.append('new file mode %s\n' % mode)
1777 header.append('new file mode %s\n' % mode)
1713 elif ctx2.flags(f):
1778 elif ctx2.flags(f):
1714 losedatafn(f)
1779 losedatafn(f)
1715 # In theory, if tn was copied or renamed we should check
1780 # In theory, if tn was copied or renamed we should check
1716 # if the source is binary too but the copy record already
1781 # if the source is binary too but the copy record already
1717 # forces git mode.
1782 # forces git mode.
1718 if util.binary(tn):
1783 if util.binary(tn):
1719 if opts.git:
1784 if opts.git:
1720 dodiff = 'binary'
1785 dodiff = 'binary'
1721 else:
1786 else:
1722 losedatafn(f)
1787 losedatafn(f)
1723 if not opts.git and not tn:
1788 if not opts.git and not tn:
1724 # regular diffs cannot represent new empty file
1789 # regular diffs cannot represent new empty file
1725 losedatafn(f)
1790 losedatafn(f)
1726 elif f in removed or (f in modified and tn is None):
1791 elif f in removed or (f in modified and tn is None):
1727 if opts.git:
1792 if opts.git:
1728 # have we already reported a copy above?
1793 # have we already reported a copy above?
1729 if ((f in copy and copy[f] in added
1794 if ((f in copy and copy[f] in added
1730 and copyto[copy[f]] == f) or
1795 and copyto[copy[f]] == f) or
1731 (f in copyto and copyto[f] in added
1796 (f in copyto and copyto[f] in added
1732 and copy[copyto[f]] == f)):
1797 and copy[copyto[f]] == f)):
1733 dodiff = False
1798 dodiff = False
1734 else:
1799 else:
1735 header.append('deleted file mode %s\n' %
1800 header.append('deleted file mode %s\n' %
1736 gitmode[man1.flags(f)])
1801 gitmode[man1.flags(f)])
1737 if util.binary(to):
1802 if util.binary(to):
1738 dodiff = 'binary'
1803 dodiff = 'binary'
1739 elif not to or util.binary(to):
1804 elif not to or util.binary(to):
1740 # regular diffs cannot represent empty file deletion
1805 # regular diffs cannot represent empty file deletion
1741 losedatafn(f)
1806 losedatafn(f)
1742 else:
1807 else:
1743 oflag = man1.flags(f)
1808 oflag = man1.flags(f)
1744 nflag = ctx2.flags(f)
1809 nflag = ctx2.flags(f)
1745 binary = util.binary(to) or util.binary(tn)
1810 binary = util.binary(to) or util.binary(tn)
1746 if opts.git:
1811 if opts.git:
1747 addmodehdr(header, gitmode[oflag], gitmode[nflag])
1812 addmodehdr(header, gitmode[oflag], gitmode[nflag])
1748 if binary:
1813 if binary:
1749 dodiff = 'binary'
1814 dodiff = 'binary'
1750 elif binary or nflag != oflag:
1815 elif binary or nflag != oflag:
1751 losedatafn(f)
1816 losedatafn(f)
1752
1817
1753 if dodiff:
1818 if dodiff:
1754 if opts.git or revs:
1819 if opts.git or revs:
1755 header.insert(0, diffline(join(a), join(b), revs))
1820 header.insert(0, diffline(join(a), join(b), revs))
1756 if dodiff == 'binary':
1821 if dodiff == 'binary':
1757 text = mdiff.b85diff(to, tn)
1822 text = mdiff.b85diff(to, tn)
1758 if text:
1823 if text:
1759 addindexmeta(header, [gitindex(to), gitindex(tn)])
1824 addindexmeta(header, [gitindex(to), gitindex(tn)])
1760 else:
1825 else:
1761 text = mdiff.unidiff(to, date1,
1826 text = mdiff.unidiff(to, date1,
1762 # ctx2 date may be dynamic
1827 # ctx2 date may be dynamic
1763 tn, util.datestr(ctx2.date()),
1828 tn, util.datestr(ctx2.date()),
1764 join(a), join(b), opts=opts)
1829 join(a), join(b), opts=opts)
1765 if header and (text or len(header) > 1):
1830 if header and (text or len(header) > 1):
1766 yield ''.join(header)
1831 yield ''.join(header)
1767 if text:
1832 if text:
1768 yield text
1833 yield text
1769
1834
1770 def diffstatsum(stats):
1835 def diffstatsum(stats):
1771 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
1836 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
1772 for f, a, r, b in stats:
1837 for f, a, r, b in stats:
1773 maxfile = max(maxfile, encoding.colwidth(f))
1838 maxfile = max(maxfile, encoding.colwidth(f))
1774 maxtotal = max(maxtotal, a + r)
1839 maxtotal = max(maxtotal, a + r)
1775 addtotal += a
1840 addtotal += a
1776 removetotal += r
1841 removetotal += r
1777 binary = binary or b
1842 binary = binary or b
1778
1843
1779 return maxfile, maxtotal, addtotal, removetotal, binary
1844 return maxfile, maxtotal, addtotal, removetotal, binary
1780
1845
1781 def diffstatdata(lines):
1846 def diffstatdata(lines):
1782 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1847 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1783
1848
1784 results = []
1849 results = []
1785 filename, adds, removes, isbinary = None, 0, 0, False
1850 filename, adds, removes, isbinary = None, 0, 0, False
1786
1851
1787 def addresult():
1852 def addresult():
1788 if filename:
1853 if filename:
1789 results.append((filename, adds, removes, isbinary))
1854 results.append((filename, adds, removes, isbinary))
1790
1855
1791 for line in lines:
1856 for line in lines:
1792 if line.startswith('diff'):
1857 if line.startswith('diff'):
1793 addresult()
1858 addresult()
1794 # set numbers to 0 anyway when starting new file
1859 # set numbers to 0 anyway when starting new file
1795 adds, removes, isbinary = 0, 0, False
1860 adds, removes, isbinary = 0, 0, False
1796 if line.startswith('diff --git a/'):
1861 if line.startswith('diff --git a/'):
1797 filename = gitre.search(line).group(1)
1862 filename = gitre.search(line).group(1)
1798 elif line.startswith('diff -r'):
1863 elif line.startswith('diff -r'):
1799 # format: "diff -r ... -r ... filename"
1864 # format: "diff -r ... -r ... filename"
1800 filename = diffre.search(line).group(1)
1865 filename = diffre.search(line).group(1)
1801 elif line.startswith('+') and not line.startswith('+++ '):
1866 elif line.startswith('+') and not line.startswith('+++ '):
1802 adds += 1
1867 adds += 1
1803 elif line.startswith('-') and not line.startswith('--- '):
1868 elif line.startswith('-') and not line.startswith('--- '):
1804 removes += 1
1869 removes += 1
1805 elif (line.startswith('GIT binary patch') or
1870 elif (line.startswith('GIT binary patch') or
1806 line.startswith('Binary file')):
1871 line.startswith('Binary file')):
1807 isbinary = True
1872 isbinary = True
1808 addresult()
1873 addresult()
1809 return results
1874 return results
1810
1875
1811 def diffstat(lines, width=80, git=False):
1876 def diffstat(lines, width=80, git=False):
1812 output = []
1877 output = []
1813 stats = diffstatdata(lines)
1878 stats = diffstatdata(lines)
1814 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
1879 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
1815
1880
1816 countwidth = len(str(maxtotal))
1881 countwidth = len(str(maxtotal))
1817 if hasbinary and countwidth < 3:
1882 if hasbinary and countwidth < 3:
1818 countwidth = 3
1883 countwidth = 3
1819 graphwidth = width - countwidth - maxname - 6
1884 graphwidth = width - countwidth - maxname - 6
1820 if graphwidth < 10:
1885 if graphwidth < 10:
1821 graphwidth = 10
1886 graphwidth = 10
1822
1887
1823 def scale(i):
1888 def scale(i):
1824 if maxtotal <= graphwidth:
1889 if maxtotal <= graphwidth:
1825 return i
1890 return i
1826 # If diffstat runs out of room it doesn't print anything,
1891 # If diffstat runs out of room it doesn't print anything,
1827 # which isn't very useful, so always print at least one + or -
1892 # which isn't very useful, so always print at least one + or -
1828 # if there were at least some changes.
1893 # if there were at least some changes.
1829 return max(i * graphwidth // maxtotal, int(bool(i)))
1894 return max(i * graphwidth // maxtotal, int(bool(i)))
1830
1895
1831 for filename, adds, removes, isbinary in stats:
1896 for filename, adds, removes, isbinary in stats:
1832 if isbinary:
1897 if isbinary:
1833 count = 'Bin'
1898 count = 'Bin'
1834 else:
1899 else:
1835 count = adds + removes
1900 count = adds + removes
1836 pluses = '+' * scale(adds)
1901 pluses = '+' * scale(adds)
1837 minuses = '-' * scale(removes)
1902 minuses = '-' * scale(removes)
1838 output.append(' %s%s | %*s %s%s\n' %
1903 output.append(' %s%s | %*s %s%s\n' %
1839 (filename, ' ' * (maxname - encoding.colwidth(filename)),
1904 (filename, ' ' * (maxname - encoding.colwidth(filename)),
1840 countwidth, count, pluses, minuses))
1905 countwidth, count, pluses, minuses))
1841
1906
1842 if stats:
1907 if stats:
1843 output.append(_(' %d files changed, %d insertions(+), '
1908 output.append(_(' %d files changed, %d insertions(+), '
1844 '%d deletions(-)\n')
1909 '%d deletions(-)\n')
1845 % (len(stats), totaladds, totalremoves))
1910 % (len(stats), totaladds, totalremoves))
1846
1911
1847 return ''.join(output)
1912 return ''.join(output)
1848
1913
1849 def diffstatui(*args, **kw):
1914 def diffstatui(*args, **kw):
1850 '''like diffstat(), but yields 2-tuples of (output, label) for
1915 '''like diffstat(), but yields 2-tuples of (output, label) for
1851 ui.write()
1916 ui.write()
1852 '''
1917 '''
1853
1918
1854 for line in diffstat(*args, **kw).splitlines():
1919 for line in diffstat(*args, **kw).splitlines():
1855 if line and line[-1] in '+-':
1920 if line and line[-1] in '+-':
1856 name, graph = line.rsplit(' ', 1)
1921 name, graph = line.rsplit(' ', 1)
1857 yield (name + ' ', '')
1922 yield (name + ' ', '')
1858 m = re.search(r'\++', graph)
1923 m = re.search(r'\++', graph)
1859 if m:
1924 if m:
1860 yield (m.group(0), 'diffstat.inserted')
1925 yield (m.group(0), 'diffstat.inserted')
1861 m = re.search(r'-+', graph)
1926 m = re.search(r'-+', graph)
1862 if m:
1927 if m:
1863 yield (m.group(0), 'diffstat.deleted')
1928 yield (m.group(0), 'diffstat.deleted')
1864 else:
1929 else:
1865 yield (line, '')
1930 yield (line, '')
1866 yield ('\n', '')
1931 yield ('\n', '')
@@ -1,611 +1,720 b''
1 $ hg init repo
1 $ hg init repo
2 $ cd repo
2 $ cd repo
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 #if execbit
43 #if execbit
44 $ hg tip -q
44 $ hg tip -q
45 2:3a34410f282e
45 2:3a34410f282e
46 $ test -x new
46 $ test -x new
47 $ hg rollback -q
47 $ hg rollback -q
48 #else
48 #else
49 $ hg tip -q
49 $ hg tip -q
50 1:ab199dc869b5
50 1:ab199dc869b5
51 #endif
51 #endif
52
52
53 Copy and removing x bit:
53 Copy and removing x bit:
54
54
55 $ hg import -f -d "1000000 0" -mcopy - <<EOF
55 $ hg import -f -d "1000000 0" -mcopy - <<EOF
56 > diff --git a/new b/copy
56 > diff --git a/new b/copy
57 > old mode 100755
57 > old mode 100755
58 > new mode 100644
58 > new mode 100644
59 > similarity index 100%
59 > similarity index 100%
60 > copy from new
60 > copy from new
61 > copy to copy
61 > copy to copy
62 > diff --git a/new b/copyx
62 > diff --git a/new b/copyx
63 > similarity index 100%
63 > similarity index 100%
64 > copy from new
64 > copy from new
65 > copy to copyx
65 > copy to copyx
66 > EOF
66 > EOF
67 applying patch from stdin
67 applying patch from stdin
68
68
69 $ test -f copy
69 $ test -f copy
70 #if execbit
70 #if execbit
71 $ test ! -x copy
71 $ test ! -x copy
72 $ test -x copyx
72 $ test -x copyx
73 $ hg tip -q
73 $ hg tip -q
74 2:21dfaae65c71
74 2:21dfaae65c71
75 #else
75 #else
76 $ hg tip -q
76 $ hg tip -q
77 2:0efdaa8e3bf3
77 2:0efdaa8e3bf3
78 #endif
78 #endif
79
79
80 $ hg up -qCr1
80 $ hg up -qCr1
81 $ hg rollback -q
81 $ hg rollback -q
82
82
83 Copy (like above but independent of execbit):
83 Copy (like above but independent of execbit):
84
84
85 $ hg import -d "1000000 0" -mcopy - <<EOF
85 $ hg import -d "1000000 0" -mcopy - <<EOF
86 > diff --git a/new b/copy
86 > diff --git a/new b/copy
87 > similarity index 100%
87 > similarity index 100%
88 > copy from new
88 > copy from new
89 > copy to copy
89 > copy to copy
90 > diff --git a/new b/copyx
90 > diff --git a/new b/copyx
91 > similarity index 100%
91 > similarity index 100%
92 > copy from new
92 > copy from new
93 > copy to copyx
93 > copy to copyx
94 > EOF
94 > EOF
95 applying patch from stdin
95 applying patch from stdin
96
96
97 $ hg tip -q
97 $ hg tip -q
98 2:0efdaa8e3bf3
98 2:0efdaa8e3bf3
99 $ test -f copy
99 $ test -f copy
100
100
101 $ cat copy
101 $ cat copy
102 a
102 a
103
103
104 $ hg cat copy
104 $ hg cat copy
105 a
105 a
106
106
107 Rename:
107 Rename:
108
108
109 $ hg import -d "1000000 0" -mrename - <<EOF
109 $ hg import -d "1000000 0" -mrename - <<EOF
110 > diff --git a/copy b/rename
110 > diff --git a/copy b/rename
111 > similarity index 100%
111 > similarity index 100%
112 > rename from copy
112 > rename from copy
113 > rename to rename
113 > rename to rename
114 > EOF
114 > EOF
115 applying patch from stdin
115 applying patch from stdin
116
116
117 $ hg tip -q
117 $ hg tip -q
118 3:b1f57753fad2
118 3:b1f57753fad2
119
119
120 $ hg locate
120 $ hg locate
121 copyx
121 copyx
122 empty
122 empty
123 new
123 new
124 rename
124 rename
125
125
126 Delete:
126 Delete:
127
127
128 $ hg import -d "1000000 0" -mdelete - <<EOF
128 $ hg import -d "1000000 0" -mdelete - <<EOF
129 > diff --git a/copyx b/copyx
129 > diff --git a/copyx b/copyx
130 > deleted file mode 100755
130 > deleted file mode 100755
131 > index 7898192..0000000
131 > index 7898192..0000000
132 > --- a/copyx
132 > --- a/copyx
133 > +++ /dev/null
133 > +++ /dev/null
134 > @@ -1 +0,0 @@
134 > @@ -1 +0,0 @@
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 4:1bd1da94b9b2
140 4:1bd1da94b9b2
141
141
142 $ hg locate
142 $ hg locate
143 empty
143 empty
144 new
144 new
145 rename
145 rename
146
146
147 $ test -f copyx
147 $ test -f copyx
148 [1]
148 [1]
149
149
150 Regular diff:
150 Regular diff:
151
151
152 $ hg import -d "1000000 0" -mregular - <<EOF
152 $ hg import -d "1000000 0" -mregular - <<EOF
153 > diff --git a/rename b/rename
153 > diff --git a/rename b/rename
154 > index 7898192..72e1fe3 100644
154 > index 7898192..72e1fe3 100644
155 > --- a/rename
155 > --- a/rename
156 > +++ b/rename
156 > +++ b/rename
157 > @@ -1 +1,5 @@
157 > @@ -1 +1,5 @@
158 > a
158 > a
159 > +a
159 > +a
160 > +a
160 > +a
161 > +a
161 > +a
162 > +a
162 > +a
163 > EOF
163 > EOF
164 applying patch from stdin
164 applying patch from stdin
165
165
166 $ hg tip -q
166 $ hg tip -q
167 5:46fe99cb3035
167 5:46fe99cb3035
168
168
169 Copy and modify:
169 Copy and modify:
170
170
171 $ hg import -d "1000000 0" -mcopymod - <<EOF
171 $ hg import -d "1000000 0" -mcopymod - <<EOF
172 > diff --git a/rename b/copy2
172 > diff --git a/rename b/copy2
173 > similarity index 80%
173 > similarity index 80%
174 > copy from rename
174 > copy from rename
175 > copy to copy2
175 > copy to copy2
176 > index 72e1fe3..b53c148 100644
176 > index 72e1fe3..b53c148 100644
177 > --- a/rename
177 > --- a/rename
178 > +++ b/copy2
178 > +++ b/copy2
179 > @@ -1,5 +1,5 @@
179 > @@ -1,5 +1,5 @@
180 > a
180 > a
181 > a
181 > a
182 > -a
182 > -a
183 > +b
183 > +b
184 > a
184 > a
185 > a
185 > a
186 > EOF
186 > EOF
187 applying patch from stdin
187 applying patch from stdin
188
188
189 $ hg tip -q
189 $ hg tip -q
190 6:ffeb3197c12d
190 6:ffeb3197c12d
191
191
192 $ hg cat copy2
192 $ hg cat copy2
193 a
193 a
194 a
194 a
195 b
195 b
196 a
196 a
197 a
197 a
198
198
199 Rename and modify:
199 Rename and modify:
200
200
201 $ hg import -d "1000000 0" -mrenamemod - <<EOF
201 $ hg import -d "1000000 0" -mrenamemod - <<EOF
202 > diff --git a/copy2 b/rename2
202 > diff --git a/copy2 b/rename2
203 > similarity index 80%
203 > similarity index 80%
204 > rename from copy2
204 > rename from copy2
205 > rename to rename2
205 > rename to rename2
206 > index b53c148..8f81e29 100644
206 > index b53c148..8f81e29 100644
207 > --- a/copy2
207 > --- a/copy2
208 > +++ b/rename2
208 > +++ b/rename2
209 > @@ -1,5 +1,5 @@
209 > @@ -1,5 +1,5 @@
210 > a
210 > a
211 > a
211 > a
212 > b
212 > b
213 > -a
213 > -a
214 > +c
214 > +c
215 > a
215 > a
216 > EOF
216 > EOF
217 applying patch from stdin
217 applying patch from stdin
218
218
219 $ hg tip -q
219 $ hg tip -q
220 7:401aede9e6bb
220 7:401aede9e6bb
221
221
222 $ hg locate copy2
222 $ hg locate copy2
223 [1]
223 [1]
224 $ hg cat rename2
224 $ hg cat rename2
225 a
225 a
226 a
226 a
227 b
227 b
228 c
228 c
229 a
229 a
230
230
231 One file renamed multiple times:
231 One file renamed multiple times:
232
232
233 $ hg import -d "1000000 0" -mmultirenames - <<EOF
233 $ hg import -d "1000000 0" -mmultirenames - <<EOF
234 > diff --git a/rename2 b/rename3
234 > diff --git a/rename2 b/rename3
235 > rename from rename2
235 > rename from rename2
236 > rename to rename3
236 > rename to rename3
237 > diff --git a/rename2 b/rename3-2
237 > diff --git a/rename2 b/rename3-2
238 > rename from rename2
238 > rename from rename2
239 > rename to rename3-2
239 > rename to rename3-2
240 > EOF
240 > EOF
241 applying patch from stdin
241 applying patch from stdin
242
242
243 $ hg tip -q
243 $ hg tip -q
244 8:2ef727e684e8
244 8:2ef727e684e8
245
245
246 $ hg log -vr. --template '{rev} {files} / {file_copies}\n'
246 $ hg log -vr. --template '{rev} {files} / {file_copies}\n'
247 8 rename2 rename3 rename3-2 / rename3 (rename2)rename3-2 (rename2)
247 8 rename2 rename3 rename3-2 / rename3 (rename2)rename3-2 (rename2)
248
248
249 $ hg locate rename2 rename3 rename3-2
249 $ hg locate rename2 rename3 rename3-2
250 rename3
250 rename3
251 rename3-2
251 rename3-2
252
252
253 $ hg cat rename3
253 $ hg cat rename3
254 a
254 a
255 a
255 a
256 b
256 b
257 c
257 c
258 a
258 a
259
259
260 $ hg cat rename3-2
260 $ hg cat rename3-2
261 a
261 a
262 a
262 a
263 b
263 b
264 c
264 c
265 a
265 a
266
266
267 $ echo foo > foo
267 $ echo foo > foo
268 $ hg add foo
268 $ hg add foo
269 $ hg ci -m 'add foo'
269 $ hg ci -m 'add foo'
270
270
271 Binary files and regular patch hunks:
271 Binary files and regular patch hunks:
272
272
273 $ hg import -d "1000000 0" -m binaryregular - <<EOF
273 $ hg import -d "1000000 0" -m binaryregular - <<EOF
274 > diff --git a/binary b/binary
274 > diff --git a/binary b/binary
275 > new file mode 100644
275 > new file mode 100644
276 > index 0000000000000000000000000000000000000000..593f4708db84ac8fd0f5cc47c634f38c013fe9e4
276 > index 0000000000000000000000000000000000000000..593f4708db84ac8fd0f5cc47c634f38c013fe9e4
277 > GIT binary patch
277 > GIT binary patch
278 > literal 4
278 > literal 4
279 > Lc\${NkU|;|M00aO5
279 > Lc\${NkU|;|M00aO5
280 >
280 >
281 > diff --git a/foo b/foo2
281 > diff --git a/foo b/foo2
282 > rename from foo
282 > rename from foo
283 > rename to foo2
283 > rename to foo2
284 > EOF
284 > EOF
285 applying patch from stdin
285 applying patch from stdin
286
286
287 $ hg tip -q
287 $ hg tip -q
288 10:27377172366e
288 10:27377172366e
289
289
290 $ cat foo2
290 $ cat foo2
291 foo
291 foo
292
292
293 $ hg manifest --debug | grep binary
293 $ hg manifest --debug | grep binary
294 045c85ba38952325e126c70962cc0f9d9077bc67 644 binary
294 045c85ba38952325e126c70962cc0f9d9077bc67 644 binary
295
295
296 Multiple binary files:
296 Multiple binary files:
297
297
298 $ hg import -d "1000000 0" -m multibinary - <<EOF
298 $ hg import -d "1000000 0" -m multibinary - <<EOF
299 > diff --git a/mbinary1 b/mbinary1
299 > diff --git a/mbinary1 b/mbinary1
300 > new file mode 100644
300 > new file mode 100644
301 > index 0000000000000000000000000000000000000000..593f4708db84ac8fd0f5cc47c634f38c013fe9e4
301 > index 0000000000000000000000000000000000000000..593f4708db84ac8fd0f5cc47c634f38c013fe9e4
302 > GIT binary patch
302 > GIT binary patch
303 > literal 4
303 > literal 4
304 > Lc\${NkU|;|M00aO5
304 > Lc\${NkU|;|M00aO5
305 >
305 >
306 > diff --git a/mbinary2 b/mbinary2
306 > diff --git a/mbinary2 b/mbinary2
307 > new file mode 100644
307 > new file mode 100644
308 > index 0000000000000000000000000000000000000000..112363ac1917b417ffbd7f376ca786a1e5fa7490
308 > index 0000000000000000000000000000000000000000..112363ac1917b417ffbd7f376ca786a1e5fa7490
309 > GIT binary patch
309 > GIT binary patch
310 > literal 5
310 > literal 5
311 > Mc\${NkU|\`?^000jF3jhEB
311 > Mc\${NkU|\`?^000jF3jhEB
312 >
312 >
313 > EOF
313 > EOF
314 applying patch from stdin
314 applying patch from stdin
315
315
316 $ hg tip -q
316 $ hg tip -q
317 11:18b73a84b4ab
317 11:18b73a84b4ab
318
318
319 $ hg manifest --debug | grep mbinary
319 $ hg manifest --debug | grep mbinary
320 045c85ba38952325e126c70962cc0f9d9077bc67 644 mbinary1
320 045c85ba38952325e126c70962cc0f9d9077bc67 644 mbinary1
321 a874b471193996e7cb034bb301cac7bdaf3e3f46 644 mbinary2
321 a874b471193996e7cb034bb301cac7bdaf3e3f46 644 mbinary2
322
322
323 Binary file and delta hunk:
324
325 $ hg import -d "1000000 0" -m delta - <<'EOF'
326 > diff --git a/delta b/delta
327 > new file mode 100644
328 > index 0000000000000000000000000000000000000000..8c9b7831b231c2600843e303e66b521353a200b3
329 > GIT binary patch
330 > literal 3749
331 > zcmV;W4qEYvP)<h;3K|Lk000e1NJLTq006iE002D*0ssI2kt{U(0000PbVXQnQ*UN;
332 > zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU=M@d9MRCwC#oC!>o#}>x{(W-y~UN*tK
333 > z%A%sxiUy2Ys)0Vm#ueArYKoYqX;GuiqZpgirM6nCVoYk?YNAz3G~z;BZ~@~&OQEe4
334 > zmGvS5isFJI;Pd_7J+EKxyHZeu`^t4r2>F;h-+VK3{_{WoGv8dSpFDYDrA%3UX03pt
335 > zOaVoi0*W#P6lDr1$`nwPDWE7*rhuYM0Y#YtiZTThWeO<D6i}2YpqR<%$s>bRRaI42
336 > zS3iFIxJ8Q=EnBv1Z7?pBw_bLjJb3V+tgP(Tty_2R-mR#p04x78n2n7MSOFyt4i1iv
337 > zjxH`PPEJmgD7U?IK&h;(EGQ@_DJc<@01=4fiNXHcKZ8LhZQ8T}E3U4tUS3}OrcgQW
338 > zWdX{K8#l7Ev&#$ysR)G#0*rC+<WGZ3?CtG4bm-ve>Dj$|_qJ`@D*stNP_AFUe&x!Q
339 > zJ9q9B7Z=ym)MyZ?Tg1ROunUYr81nV?B@!tYS~5_|%gfW#(_s<4UN1!Q?Dv8d>g#m6
340 > z%*@R2@bI2JdnzxQ!EDU`$eQY!tgI~Zn$prz;gaXNod5*5p(1Bz=P$qfvZ$y?dC@X~
341 > zlAD+NAKhB{=;6bMwzjqn>9mavvKOGd`s%A+fBiL>Q;xJWpa72C+}u{JTHUX>{~}Qj
342 > zUb%hyHgN~c?cBLjInvUALMD9g-aXt54ZL8AOCvXL-V6!~ijR*kEG$&Mv?!pE61OlI
343 > z8nzMSPE8F7bH|Py*RNl1VUCggq<V)>@_6gkEeiz7{rmTeuNTW6+KVS#0FG%IHf-3L
344 > zGiS21vn>WCCr+GLx^!uNetzB6u3o(w6&1C2?_LW8ij$+$sZ*zZ`|US3H@8N~%&V%Z
345 > zAeA0HdhFS=$6|nzn3%YH`SN<>DQRO;Qc^)dfdvA^5u`Xf;Zzu<ZQHgG?28V-#s<;T
346 > zzkh#LA)v7gpoE5ou3o*GoUUF%b#iht&kl9d0)><$FE1}ACr68;uCA`6DrGmz_U+rp
347 > zL>Rx;X_yhk$fP_yJrTCQ|NgsW0A<985g&c@k-NKly<>mgU8n||ZPPV<`SN8#%$+-T
348 > zfP$T!ou8jypFVwnzqhxyUvIxXd-wF~*U!ht=hCH1wzjqn9x#)IrhDa;S0JbK^z_$W
349 > zd(8rX@;7|t*;GJ5h$SZ{v(}+UBEs$4w~?{@9%`_Z<P<kox5bMWuUWH(sF9hONgd$Q
350 > zunCgwT@1|CU9+;X^4z&|M~@yw23Ay50NFWn=FqF%yLZEUty;AT2??1oV@B)Nt))J7
351 > zh>{5j2@f7T=-an%L_`E)h;mZ4D_5>?7tjQtVPRo2XU-&;mX(!l-MSTJP4XWY82JAC
352 > z@57+y&!1=P{Mn{W8)-HzEsgAtd63}Cazc>O6vGb>51%@9DzbyI3?4j~$ijmT95_IS
353 > zS#r!LCDW%*4-O7CGnkr$xXR1RQ&UrA<CQt}^73NL%zk`)Jk!yxUAt-1r}ggLn-Zq}
354 > z*s){8pw68;i+kiG%CpBKYSJLLFyq&*U8}qDp+kpe&6<Vp(Z58%l#~>ZK?&s7y?b}i
355 > zuwcOgO%x-27A;y785zknl_{sU;E6v$8{pWmVS{KaJPpu`i;HP$#flY@u~Ua~K3%tN
356 > z-LhrNh{9SoHgDd%WXTc$$~Dq{?AWou3!H&?V8K{^{P9Ot5vecD?%1&-E-ntBFj87(
357 > zy5`QE%QRX7qcHC%1{Ua}M~}L6=`wQUNEQ=I;qc+ZMMXtK2T+0os;jEco;}OV9z1w3
358 > zARqv^bm-85xnRCng3OT|MyVSmR3ND7^?KaQGG!^(aTbo1N;Nz;X3Q9FJbwK6`0?Yp
359 > zj*X2ac;Pw3!I2|JShDaF>-gJmzm1NLj){rk&o|$E^WAsfrK=x&@B!`w7Hik81sPz4
360 > zuJTaiCppM>-+c!wPzcUw)5@?J4U-u|pJ~xbWUe-C+60k^7>9!)56DbjmA~`OJJ40v
361 > zu3hCA7eJXZWeN|1iJLu87$;+fS8+Kq6O`aT)*_x@sY#t7LxwoEcVw*)cWhhQW@l%!
362 > z{#Z=y+qcK@%z{p*D=8_Fcg278AnH3fI5;~yGu?9TscxXaaP*4$f<LIv!^5Lfr%vKg
363 > zpxmunH#%=+ICMvZA~wyNH%~eMl!-g^R!cYJ#WmLq5N8viz#J%%LPtkO?V)tZ81cp>
364 > z{ALK?fNPePmd;289&M8Q3>YwgZX5GcGY&n>K1<x)!`;Qjg&}bb!Lrnl@xH#kS~VYE
365 > zpJmIJO`A3iy+Y3X`k>cY-@}Iw2Onq`=!ba3eATgs3yg3Wej=+P-Z8WF#w=RXvS@J3
366 > zEyhVTj-gO?kfDu1g9afo<RkPrYzG#_yF41IFxF%Ylg>9lx6<clPweR-b7Hn+r)e1l
367 > zO6c6FbNt@;;*w$z;N|H>h{czme)_4V6UC4hv**kX2@L^Bgds$(&P7M4dhfmWe)!=B
368 > zR3X=Y{P9N}p@-##@1ZNW1YbVaiP~D@8m&<dzEP&cO|87Ju#j*=;wH~Exr>i*Hpp&@
369 > z`9!Sj+O;byD~s8qZ>6QB8uv7Bpn&&?xe;;e<M4F8KEID&pT7QmqoSgq&06adp5T=U
370 > z6DH*4=AB7C1D9Amu?ia-wtxSAlmTEO96XHx)-+rKP;ip$pukuSJGW3P1aUmc2yo%)
371 > z&<t3F>d1X+1qzaag-%x+eKHx{?Afz3GBQSw9u0lw<mB+I#v11TKRpKWQS+lvVL7=u
372 > zHr6)1ynEF<i3kO6A8&ppPMo-F=PnWfXkSj@i*7J6C<F}wR?s(O0niC?t+6;+k}pPq
373 > zrok&TPU40rL0ZYDwenNrrmPZ`gjo@DEF`7^cKP||pUr;+r)hyn9O37=xA`3%Bj-ih
374 > z+1usk<%5G-y+R?tA`qY=)6&vNjL{P?QzHg%P%>`ZxP=QB%DHY6L26?36V_p^{}n$q
375 > z3@9W=KmGI*Ng_Q#AzA%-z|Z^|#oW(hkfgpuS$RKRhlrarX%efMMCs}GLChec5+y{6
376 > z1Qnxim_C-fmQuaAK_NUHUBV&;1c0V)wji<RcdZ*aAWTwyt>hVnlt^asFCe0&a@tqp
377 > zEEy;$L}D$X6)wfQNl8gu6Z>oB3_RrP=gTyK2@@w#LbQfLNHj>Q&z(C5wUFhK+}0aV
378 > zSohlc=7K+spN<ctf}5KgKqNyJDNP9;LZd)nTE=9|6Xdr9%Hzk63-tL2c9FD*rsyYY
379 > z!}t+Yljq7-p$X;4_YL?6d;mdY3R##o1e%rlPxrsMh8|;sKTr~^QD#sw3&vS$FwlTk
380 > zp1#Gw!Qo-$LtvpXt#ApV0g)^F=qFB`VB!W297x=$mr<$>rco3v$QKih_xN!k6;M=@
381 > zCr?gDNQj7tm@;JwD;Ty&NlBSCYZk(b3dZeN8D4h2{r20dSFc7;(>E&r`s=TVtzpB4
382 > zk+^N&zCAiRns(?p6iBlk9v&h{1ve(FNtc)td51M>)TkXhc6{>5C)`fS$&)A1*CP1%
383 > zld+peue4aYbg3C0!+4mu+}vE^j_feX+ZijvffBI7Ofh#RZ*U3<3J5(+nfRCzexqQ5
384 > zgM&##Y4Dd{e%ZKjqrbm@|Ni}l4jo!AqtFynj3Xsd$o^?yV4$|UQ(j&UWCH>M=o_&N
385 > zmclXc3i|Q#<;#EoG>~V}4unTHbUK}u=y4;rA3S&vzC3^aJP!&D4RvvGfoyo(>C>la
386 > zijP<=v>X{3Ne&2BXo}DV8l0V-jdv`$am0ubG{Wuh%CTd|l9Q7m;G&|U@#Dvbhlj(d
387 > zg6W{3ATxYt#T?)3;SmIgOP4M|Dki~I_TX7SxP0x}wI~DQI7Lhm2BI7gph(aPIFAd;
388 > zQ&UsF`Q{rOz+z=87c5v%@5u~d6dWV5OlX`oH3cAH&UlvsZUEo(Q(P|lKs17rXvaiU
389 > zQcj}IEufi1+Bnh6&(EhF{7O3vLHp`jjlp0J<M1kh$+$2xGm~Zk7OY7(q=&Rdhq*RG
390 > zwrmcd5MnP}xByB_)P@{J>DR9x6;`cUwPM8z){yooNiXPOc9_{W-gtwxE5TUg0vJk6
391 > zO#JGruV&1cL6VGK2?+_YQr4`+EY8;Sm$9U$uuGRN=uj3k7?O9b+R~J7t_y*K64ZnI
392 > zM+{aE<b(v?vSmw;9zFP!aE266zHIhlmdI@^xa6o2jwdRk54a$>pcRbC29ZyG!Cfdp
393 > zutFf`Q`vljgo!(wHf=)F#m2_MIuj;L(2ja2YsQRX+rswV{d<H`Ar;(@%aNa9VPU8Z
394 > z;tq*`y}dm#NDJHKlV}uTIm!_vAq5E7!X-p{P=Z=Sh668>PuVS1*6e}OwOiMc;u3OQ
395 > z@Bs)w3=lzfKoufH$SFuPG@uZ4NOnM#+=8LnQ2Q4zUd+nM+OT26;lqbN{P07dhH{jH
396 > zManE8^dLms-Q2;1kB<*Q1a3f8kZr;xX=!Qro@`~@xN*Qj>gx;i;0Z24!~i2uLb`}v
397 > zA?R$|wvC+m^Ups=*(4lDh*=UN8{5h(A?p#D^2N$8u4Z55!q?ZAh(iEEng9_Zi>IgO
398 > z#~**JC8hE4@n{hO&8btT5F*?nC_%LhA3i)PDhh-pB_&1wGrDIl^*=8x3n&;akBf^-
399 > zJd&86kq$%%907v^tgWoQdwI`|oNK%VvU~S#C<o^F?6c48?Cjj#-4P<>HFD%&|Ni~t
400 > zKJ(|#H`$<5W+6ZkBb213rXonKZLB+X>^L}J@W6osP3piLD_5?R!`S}*{xLBzFiL4@
401 > zX+}l{`A%?f@T5tT%ztu60p;)be`fWC`tP@WpO=?cpf8Xuf1OSj6d3f@Ki(ovDYq%0
402 > z{4ZSe`kOay5@=lAT!}vFzxyemC{sXDrhuYM0Y#ZI1r%ipD9W11{w=@&xgJ}t2x;ep
403 > P00000NkvXXu0mjfZ5|Er
404 >
405 > literal 0
406 > HcmV?d00001
407 >
408 > EOF
409 applying patch from stdin
410
411 $ hg manifest --debug | grep delta
412 9600f98bb60ce732634d126aaa4ac1ec959c573e 644 delta
413
414 $ hg import -d "1000000 0" -m delta - <<'EOF'
415 > diff --git a/delta b/delta
416 > index 8c9b7831b231c2600843e303e66b521353a200b3..0021dd95bc0dba53c39ce81377126d43731d68df 100644
417 > GIT binary patch
418 > delta 49
419 > zcmZ1~yHs|=21Z8J$r~9bFdA-lVv=EEw4WT$qRf2QSa5SIOAHI6(&k4T8H|kLo4vWB
420 > FSO9ZT4bA`n
421 >
422 > delta 49
423 > zcmV-10M7rV9i<(xumJ(}ld%Di0Xefm0vrMXpOaq%BLm9I%d>?9Tm%6Vv*HM70RcC&
424 > HOA1;9yU-AD
425 >
426 > EOF
427 applying patch from stdin
428
429 $ hg manifest --debug | grep delta
430 56094bbea136dcf8dbd4088f6af469bde1a98b75 644 delta
431
323 Filenames with spaces:
432 Filenames with spaces:
324
433
325 $ sed 's,EOL$,,g' <<EOF | hg import -d "1000000 0" -m spaces -
434 $ sed 's,EOL$,,g' <<EOF | hg import -d "1000000 0" -m spaces -
326 > diff --git a/foo bar b/foo bar
435 > diff --git a/foo bar b/foo bar
327 > new file mode 100644
436 > new file mode 100644
328 > index 0000000..257cc56
437 > index 0000000..257cc56
329 > --- /dev/null
438 > --- /dev/null
330 > +++ b/foo bar EOL
439 > +++ b/foo bar EOL
331 > @@ -0,0 +1 @@
440 > @@ -0,0 +1 @@
332 > +foo
441 > +foo
333 > EOF
442 > EOF
334 applying patch from stdin
443 applying patch from stdin
335
444
336 $ hg tip -q
445 $ hg tip -q
337 12:47500ce1614e
446 14:4b79479c9a6d
338
447
339 $ cat "foo bar"
448 $ cat "foo bar"
340 foo
449 foo
341
450
342 Copy then modify the original file:
451 Copy then modify the original file:
343
452
344 $ hg import -d "1000000 0" -m copy-mod-orig - <<EOF
453 $ hg import -d "1000000 0" -m copy-mod-orig - <<EOF
345 > diff --git a/foo2 b/foo2
454 > diff --git a/foo2 b/foo2
346 > index 257cc56..fe08ec6 100644
455 > index 257cc56..fe08ec6 100644
347 > --- a/foo2
456 > --- a/foo2
348 > +++ b/foo2
457 > +++ b/foo2
349 > @@ -1 +1,2 @@
458 > @@ -1 +1,2 @@
350 > foo
459 > foo
351 > +new line
460 > +new line
352 > diff --git a/foo2 b/foo3
461 > diff --git a/foo2 b/foo3
353 > similarity index 100%
462 > similarity index 100%
354 > copy from foo2
463 > copy from foo2
355 > copy to foo3
464 > copy to foo3
356 > EOF
465 > EOF
357 applying patch from stdin
466 applying patch from stdin
358
467
359 $ hg tip -q
468 $ hg tip -q
360 13:6757efb07ea9
469 15:9cbe44af4ae9
361
470
362 $ cat foo3
471 $ cat foo3
363 foo
472 foo
364
473
365 Move text file and patch as binary
474 Move text file and patch as binary
366
475
367 $ echo a > text2
476 $ echo a > text2
368 $ hg ci -Am0
477 $ hg ci -Am0
369 adding text2
478 adding text2
370 $ hg import -d "1000000 0" -m rename-as-binary - <<"EOF"
479 $ hg import -d "1000000 0" -m rename-as-binary - <<"EOF"
371 > diff --git a/text2 b/binary2
480 > diff --git a/text2 b/binary2
372 > rename from text2
481 > rename from text2
373 > rename to binary2
482 > rename to binary2
374 > index 78981922613b2afb6025042ff6bd878ac1994e85..10efcb362e9f3b3420fcfbfc0e37f3dc16e29757
483 > index 78981922613b2afb6025042ff6bd878ac1994e85..10efcb362e9f3b3420fcfbfc0e37f3dc16e29757
375 > GIT binary patch
484 > GIT binary patch
376 > literal 5
485 > literal 5
377 > Mc$`b*O5$Pw00T?_*Z=?k
486 > Mc$`b*O5$Pw00T?_*Z=?k
378 >
487 >
379 > EOF
488 > EOF
380 applying patch from stdin
489 applying patch from stdin
381
490
382 $ cat binary2
491 $ cat binary2
383 a
492 a
384 b
493 b
385 \x00 (no-eol) (esc)
494 \x00 (no-eol) (esc)
386
495
387 $ hg st --copies --change .
496 $ hg st --copies --change .
388 A binary2
497 A binary2
389 text2
498 text2
390 R text2
499 R text2
391
500
392 Invalid base85 content
501 Invalid base85 content
393
502
394 $ hg rollback
503 $ hg rollback
395 repository tip rolled back to revision 14 (undo import)
504 repository tip rolled back to revision 16 (undo import)
396 working directory now based on revision 14
505 working directory now based on revision 16
397 $ hg revert -aq
506 $ hg revert -aq
398 $ hg import -d "1000000 0" -m invalid-binary - <<"EOF"
507 $ hg import -d "1000000 0" -m invalid-binary - <<"EOF"
399 > diff --git a/text2 b/binary2
508 > diff --git a/text2 b/binary2
400 > rename from text2
509 > rename from text2
401 > rename to binary2
510 > rename to binary2
402 > index 78981922613b2afb6025042ff6bd878ac1994e85..10efcb362e9f3b3420fcfbfc0e37f3dc16e29757
511 > index 78981922613b2afb6025042ff6bd878ac1994e85..10efcb362e9f3b3420fcfbfc0e37f3dc16e29757
403 > GIT binary patch
512 > GIT binary patch
404 > literal 5
513 > literal 5
405 > Mc$`b*O.$Pw00T?_*Z=?k
514 > Mc$`b*O.$Pw00T?_*Z=?k
406 >
515 >
407 > EOF
516 > EOF
408 applying patch from stdin
517 applying patch from stdin
409 abort: could not decode "binary2" binary patch: bad base85 character at position 6
518 abort: could not decode "binary2" binary patch: bad base85 character at position 6
410 [255]
519 [255]
411
520
412 $ hg revert -aq
521 $ hg revert -aq
413 $ hg import -d "1000000 0" -m rename-as-binary - <<"EOF"
522 $ hg import -d "1000000 0" -m rename-as-binary - <<"EOF"
414 > diff --git a/text2 b/binary2
523 > diff --git a/text2 b/binary2
415 > rename from text2
524 > rename from text2
416 > rename to binary2
525 > rename to binary2
417 > index 78981922613b2afb6025042ff6bd878ac1994e85..10efcb362e9f3b3420fcfbfc0e37f3dc16e29757
526 > index 78981922613b2afb6025042ff6bd878ac1994e85..10efcb362e9f3b3420fcfbfc0e37f3dc16e29757
418 > GIT binary patch
527 > GIT binary patch
419 > literal 6
528 > literal 6
420 > Mc$`b*O5$Pw00T?_*Z=?k
529 > Mc$`b*O5$Pw00T?_*Z=?k
421 >
530 >
422 > EOF
531 > EOF
423 applying patch from stdin
532 applying patch from stdin
424 abort: "binary2" length is 5 bytes, should be 6
533 abort: "binary2" length is 5 bytes, should be 6
425 [255]
534 [255]
426
535
427 $ hg revert -aq
536 $ hg revert -aq
428 $ hg import -d "1000000 0" -m rename-as-binary - <<"EOF"
537 $ hg import -d "1000000 0" -m rename-as-binary - <<"EOF"
429 > diff --git a/text2 b/binary2
538 > diff --git a/text2 b/binary2
430 > rename from text2
539 > rename from text2
431 > rename to binary2
540 > rename to binary2
432 > index 78981922613b2afb6025042ff6bd878ac1994e85..10efcb362e9f3b3420fcfbfc0e37f3dc16e29757
541 > index 78981922613b2afb6025042ff6bd878ac1994e85..10efcb362e9f3b3420fcfbfc0e37f3dc16e29757
433 > GIT binary patch
542 > GIT binary patch
434 > Mc$`b*O5$Pw00T?_*Z=?k
543 > Mc$`b*O5$Pw00T?_*Z=?k
435 >
544 >
436 > EOF
545 > EOF
437 applying patch from stdin
546 applying patch from stdin
438 abort: could not extract "binary2" binary data
547 abort: could not extract "binary2" binary data
439 [255]
548 [255]
440
549
441 Simulate a copy/paste turning LF into CRLF (issue2870)
550 Simulate a copy/paste turning LF into CRLF (issue2870)
442
551
443 $ hg revert -aq
552 $ hg revert -aq
444 $ cat > binary.diff <<"EOF"
553 $ cat > binary.diff <<"EOF"
445 > diff --git a/text2 b/binary2
554 > diff --git a/text2 b/binary2
446 > rename from text2
555 > rename from text2
447 > rename to binary2
556 > rename to binary2
448 > index 78981922613b2afb6025042ff6bd878ac1994e85..10efcb362e9f3b3420fcfbfc0e37f3dc16e29757
557 > index 78981922613b2afb6025042ff6bd878ac1994e85..10efcb362e9f3b3420fcfbfc0e37f3dc16e29757
449 > GIT binary patch
558 > GIT binary patch
450 > literal 5
559 > literal 5
451 > Mc$`b*O5$Pw00T?_*Z=?k
560 > Mc$`b*O5$Pw00T?_*Z=?k
452 >
561 >
453 > EOF
562 > EOF
454 >>> fp = file('binary.diff', 'rb')
563 >>> fp = file('binary.diff', 'rb')
455 >>> data = fp.read()
564 >>> data = fp.read()
456 >>> fp.close()
565 >>> fp.close()
457 >>> file('binary.diff', 'wb').write(data.replace('\n', '\r\n'))
566 >>> file('binary.diff', 'wb').write(data.replace('\n', '\r\n'))
458 $ rm binary2
567 $ rm binary2
459 $ hg import --no-commit binary.diff
568 $ hg import --no-commit binary.diff
460 applying binary.diff
569 applying binary.diff
461
570
462 $ cd ..
571 $ cd ..
463
572
464 Consecutive import with renames (issue2459)
573 Consecutive import with renames (issue2459)
465
574
466 $ hg init issue2459
575 $ hg init issue2459
467 $ cd issue2459
576 $ cd issue2459
468 $ hg import --no-commit --force - <<EOF
577 $ hg import --no-commit --force - <<EOF
469 > diff --git a/a b/a
578 > diff --git a/a b/a
470 > new file mode 100644
579 > new file mode 100644
471 > EOF
580 > EOF
472 applying patch from stdin
581 applying patch from stdin
473 $ hg import --no-commit --force - <<EOF
582 $ hg import --no-commit --force - <<EOF
474 > diff --git a/a b/b
583 > diff --git a/a b/b
475 > rename from a
584 > rename from a
476 > rename to b
585 > rename to b
477 > EOF
586 > EOF
478 applying patch from stdin
587 applying patch from stdin
479 a has not been committed yet, so no copy data will be stored for b.
588 a has not been committed yet, so no copy data will be stored for b.
480 $ hg debugstate
589 $ hg debugstate
481 a 0 -1 unset b
590 a 0 -1 unset b
482 $ hg ci -m done
591 $ hg ci -m done
483 $ cd ..
592 $ cd ..
484
593
485 Renames and strip
594 Renames and strip
486
595
487 $ hg init renameandstrip
596 $ hg init renameandstrip
488 $ cd renameandstrip
597 $ cd renameandstrip
489 $ echo a > a
598 $ echo a > a
490 $ hg ci -Am adda
599 $ hg ci -Am adda
491 adding a
600 adding a
492 $ hg import --no-commit -p2 - <<EOF
601 $ hg import --no-commit -p2 - <<EOF
493 > diff --git a/foo/a b/foo/b
602 > diff --git a/foo/a b/foo/b
494 > rename from foo/a
603 > rename from foo/a
495 > rename to foo/b
604 > rename to foo/b
496 > EOF
605 > EOF
497 applying patch from stdin
606 applying patch from stdin
498 $ hg st --copies
607 $ hg st --copies
499 A b
608 A b
500 a
609 a
501 R a
610 R a
502
611
503 Renames, similarity and git diff
612 Renames, similarity and git diff
504
613
505 $ hg revert -aC
614 $ hg revert -aC
506 undeleting a
615 undeleting a
507 forgetting b
616 forgetting b
508 $ rm b
617 $ rm b
509 $ hg import --similarity 90 --no-commit - <<EOF
618 $ hg import --similarity 90 --no-commit - <<EOF
510 > diff --git a/a b/b
619 > diff --git a/a b/b
511 > rename from a
620 > rename from a
512 > rename to b
621 > rename to b
513 > EOF
622 > EOF
514 applying patch from stdin
623 applying patch from stdin
515 $ hg st --copies
624 $ hg st --copies
516 A b
625 A b
517 a
626 a
518 R a
627 R a
519 $ cd ..
628 $ cd ..
520
629
521 Pure copy with existing destination
630 Pure copy with existing destination
522
631
523 $ hg init copytoexisting
632 $ hg init copytoexisting
524 $ cd copytoexisting
633 $ cd copytoexisting
525 $ echo a > a
634 $ echo a > a
526 $ echo b > b
635 $ echo b > b
527 $ hg ci -Am add
636 $ hg ci -Am add
528 adding a
637 adding a
529 adding b
638 adding b
530 $ hg import --no-commit - <<EOF
639 $ hg import --no-commit - <<EOF
531 > diff --git a/a b/b
640 > diff --git a/a b/b
532 > copy from a
641 > copy from a
533 > copy to b
642 > copy to b
534 > EOF
643 > EOF
535 applying patch from stdin
644 applying patch from stdin
536 abort: cannot create b: destination already exists
645 abort: cannot create b: destination already exists
537 [255]
646 [255]
538 $ cat b
647 $ cat b
539 b
648 b
540
649
541 Copy and changes with existing destination
650 Copy and changes with existing destination
542
651
543 $ hg import --no-commit - <<EOF
652 $ hg import --no-commit - <<EOF
544 > diff --git a/a b/b
653 > diff --git a/a b/b
545 > copy from a
654 > copy from a
546 > copy to b
655 > copy to b
547 > --- a/a
656 > --- a/a
548 > +++ b/b
657 > +++ b/b
549 > @@ -1,1 +1,2 @@
658 > @@ -1,1 +1,2 @@
550 > a
659 > a
551 > +b
660 > +b
552 > EOF
661 > EOF
553 applying patch from stdin
662 applying patch from stdin
554 cannot create b: destination already exists
663 cannot create b: destination already exists
555 1 out of 1 hunks FAILED -- saving rejects to file b.rej
664 1 out of 1 hunks FAILED -- saving rejects to file b.rej
556 abort: patch failed to apply
665 abort: patch failed to apply
557 [255]
666 [255]
558 $ cat b
667 $ cat b
559 b
668 b
560
669
561 #if symlink
670 #if symlink
562
671
563 $ ln -s b linkb
672 $ ln -s b linkb
564 $ hg add linkb
673 $ hg add linkb
565 $ hg ci -m addlinkb
674 $ hg ci -m addlinkb
566 $ hg import --no-commit - <<EOF
675 $ hg import --no-commit - <<EOF
567 > diff --git a/linkb b/linkb
676 > diff --git a/linkb b/linkb
568 > deleted file mode 120000
677 > deleted file mode 120000
569 > --- a/linkb
678 > --- a/linkb
570 > +++ /dev/null
679 > +++ /dev/null
571 > @@ -1,1 +0,0 @@
680 > @@ -1,1 +0,0 @@
572 > -badhunk
681 > -badhunk
573 > \ No newline at end of file
682 > \ No newline at end of file
574 > EOF
683 > EOF
575 applying patch from stdin
684 applying patch from stdin
576 patching file linkb
685 patching file linkb
577 Hunk #1 FAILED at 0
686 Hunk #1 FAILED at 0
578 1 out of 1 hunks FAILED -- saving rejects to file linkb.rej
687 1 out of 1 hunks FAILED -- saving rejects to file linkb.rej
579 abort: patch failed to apply
688 abort: patch failed to apply
580 [255]
689 [255]
581 $ hg st
690 $ hg st
582 ? b.rej
691 ? b.rej
583 ? linkb.rej
692 ? linkb.rej
584
693
585 #endif
694 #endif
586
695
587 Test corner case involving copies and multiple hunks (issue3384)
696 Test corner case involving copies and multiple hunks (issue3384)
588
697
589 $ hg revert -qa
698 $ hg revert -qa
590 $ hg import --no-commit - <<EOF
699 $ hg import --no-commit - <<EOF
591 > diff --git a/a b/c
700 > diff --git a/a b/c
592 > copy from a
701 > copy from a
593 > copy to c
702 > copy to c
594 > --- a/a
703 > --- a/a
595 > +++ b/c
704 > +++ b/c
596 > @@ -1,1 +1,2 @@
705 > @@ -1,1 +1,2 @@
597 > a
706 > a
598 > +a
707 > +a
599 > @@ -2,1 +2,2 @@
708 > @@ -2,1 +2,2 @@
600 > a
709 > a
601 > +a
710 > +a
602 > diff --git a/a b/a
711 > diff --git a/a b/a
603 > --- a/a
712 > --- a/a
604 > +++ b/a
713 > +++ b/a
605 > @@ -1,1 +1,2 @@
714 > @@ -1,1 +1,2 @@
606 > a
715 > a
607 > +b
716 > +b
608 > EOF
717 > EOF
609 applying patch from stdin
718 applying patch from stdin
610
719
611 $ cd ..
720 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now