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