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