##// END OF EJS Templates
Merge with crew-stable
Patrick Mezard -
r10204:3ca8f2ae merge default
parent child Browse files
Show More
@@ -1,1529 +1,1531 b''
1 # patch.py - patch file parsing routines
1 # patch.py - patch file parsing routines
2 #
2 #
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2, incorporated herein by reference.
7 # GNU General Public License version 2, incorporated herein by reference.
8
8
9 from i18n import _
9 from i18n import _
10 from node import hex, nullid, short
10 from node import hex, nullid, short
11 import base85, cmdutil, mdiff, util, diffhelpers, copies
11 import base85, cmdutil, mdiff, util, diffhelpers, copies
12 import cStringIO, email.Parser, os, re
12 import cStringIO, email.Parser, os, re
13 import sys, tempfile, zlib
13 import sys, tempfile, zlib
14
14
15 gitre = re.compile('diff --git a/(.*) b/(.*)')
15 gitre = re.compile('diff --git a/(.*) b/(.*)')
16
16
17 class PatchError(Exception):
17 class PatchError(Exception):
18 pass
18 pass
19
19
20 class NoHunks(PatchError):
20 class NoHunks(PatchError):
21 pass
21 pass
22
22
23 # helper functions
23 # helper functions
24
24
25 def copyfile(src, dst, basedir):
25 def copyfile(src, dst, basedir):
26 abssrc, absdst = [util.canonpath(basedir, basedir, x) for x in [src, dst]]
26 abssrc, absdst = [util.canonpath(basedir, basedir, x) for x in [src, dst]]
27 if os.path.exists(absdst):
27 if os.path.exists(absdst):
28 raise util.Abort(_("cannot create %s: destination already exists") %
28 raise util.Abort(_("cannot create %s: destination already exists") %
29 dst)
29 dst)
30
30
31 dstdir = os.path.dirname(absdst)
31 dstdir = os.path.dirname(absdst)
32 if dstdir and not os.path.isdir(dstdir):
32 if dstdir and not os.path.isdir(dstdir):
33 try:
33 try:
34 os.makedirs(dstdir)
34 os.makedirs(dstdir)
35 except IOError:
35 except IOError:
36 raise util.Abort(
36 raise util.Abort(
37 _("cannot create %s: unable to create destination directory")
37 _("cannot create %s: unable to create destination directory")
38 % dst)
38 % dst)
39
39
40 util.copyfile(abssrc, absdst)
40 util.copyfile(abssrc, absdst)
41
41
42 # public functions
42 # public functions
43
43
44 def extract(ui, fileobj):
44 def extract(ui, fileobj):
45 '''extract patch from data read from fileobj.
45 '''extract patch from data read from fileobj.
46
46
47 patch can be a normal patch or contained in an email message.
47 patch can be a normal patch or contained in an email message.
48
48
49 return tuple (filename, message, user, date, node, p1, p2).
49 return tuple (filename, message, user, date, node, p1, p2).
50 Any item in the returned tuple can be None. If filename is None,
50 Any item in the returned tuple can be None. If filename is None,
51 fileobj did not contain a patch. Caller must unlink filename when done.'''
51 fileobj did not contain a patch. Caller must unlink filename when done.'''
52
52
53 # attempt to detect the start of a patch
53 # attempt to detect the start of a patch
54 # (this heuristic is borrowed from quilt)
54 # (this heuristic is borrowed from quilt)
55 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
55 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
56 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
56 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
57 r'(---|\*\*\*)[ \t])', re.MULTILINE)
57 r'(---|\*\*\*)[ \t])', re.MULTILINE)
58
58
59 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
59 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
60 tmpfp = os.fdopen(fd, 'w')
60 tmpfp = os.fdopen(fd, 'w')
61 try:
61 try:
62 msg = email.Parser.Parser().parse(fileobj)
62 msg = email.Parser.Parser().parse(fileobj)
63
63
64 subject = msg['Subject']
64 subject = msg['Subject']
65 user = msg['From']
65 user = msg['From']
66 if not subject and not user:
66 if not subject and not user:
67 # Not an email, restore parsed headers if any
67 # Not an email, restore parsed headers if any
68 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
68 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
69
69
70 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
70 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
71 # should try to parse msg['Date']
71 # should try to parse msg['Date']
72 date = None
72 date = None
73 nodeid = None
73 nodeid = None
74 branch = None
74 branch = None
75 parents = []
75 parents = []
76
76
77 if subject:
77 if subject:
78 if subject.startswith('[PATCH'):
78 if subject.startswith('[PATCH'):
79 pend = subject.find(']')
79 pend = subject.find(']')
80 if pend >= 0:
80 if pend >= 0:
81 subject = subject[pend+1:].lstrip()
81 subject = subject[pend+1:].lstrip()
82 subject = subject.replace('\n\t', ' ')
82 subject = subject.replace('\n\t', ' ')
83 ui.debug('Subject: %s\n' % subject)
83 ui.debug('Subject: %s\n' % subject)
84 if user:
84 if user:
85 ui.debug('From: %s\n' % user)
85 ui.debug('From: %s\n' % user)
86 diffs_seen = 0
86 diffs_seen = 0
87 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
87 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
88 message = ''
88 message = ''
89 for part in msg.walk():
89 for part in msg.walk():
90 content_type = part.get_content_type()
90 content_type = part.get_content_type()
91 ui.debug('Content-Type: %s\n' % content_type)
91 ui.debug('Content-Type: %s\n' % content_type)
92 if content_type not in ok_types:
92 if content_type not in ok_types:
93 continue
93 continue
94 payload = part.get_payload(decode=True)
94 payload = part.get_payload(decode=True)
95 m = diffre.search(payload)
95 m = diffre.search(payload)
96 if m:
96 if m:
97 hgpatch = False
97 hgpatch = False
98 ignoretext = False
98 ignoretext = False
99
99
100 ui.debug('found patch at byte %d\n' % m.start(0))
100 ui.debug('found patch at byte %d\n' % m.start(0))
101 diffs_seen += 1
101 diffs_seen += 1
102 cfp = cStringIO.StringIO()
102 cfp = cStringIO.StringIO()
103 for line in payload[:m.start(0)].splitlines():
103 for line in payload[:m.start(0)].splitlines():
104 if line.startswith('# HG changeset patch'):
104 if line.startswith('# HG changeset patch'):
105 ui.debug('patch generated by hg export\n')
105 ui.debug('patch generated by hg export\n')
106 hgpatch = True
106 hgpatch = True
107 # drop earlier commit message content
107 # drop earlier commit message content
108 cfp.seek(0)
108 cfp.seek(0)
109 cfp.truncate()
109 cfp.truncate()
110 subject = None
110 subject = None
111 elif hgpatch:
111 elif hgpatch:
112 if line.startswith('# User '):
112 if line.startswith('# User '):
113 user = line[7:]
113 user = line[7:]
114 ui.debug('From: %s\n' % user)
114 ui.debug('From: %s\n' % user)
115 elif line.startswith("# Date "):
115 elif line.startswith("# Date "):
116 date = line[7:]
116 date = line[7:]
117 elif line.startswith("# Branch "):
117 elif line.startswith("# Branch "):
118 branch = line[9:]
118 branch = line[9:]
119 elif line.startswith("# Node ID "):
119 elif line.startswith("# Node ID "):
120 nodeid = line[10:]
120 nodeid = line[10:]
121 elif line.startswith("# Parent "):
121 elif line.startswith("# Parent "):
122 parents.append(line[10:])
122 parents.append(line[10:])
123 elif line == '---' and gitsendmail:
123 elif line == '---' and gitsendmail:
124 ignoretext = True
124 ignoretext = True
125 if not line.startswith('# ') and not ignoretext:
125 if not line.startswith('# ') and not ignoretext:
126 cfp.write(line)
126 cfp.write(line)
127 cfp.write('\n')
127 cfp.write('\n')
128 message = cfp.getvalue()
128 message = cfp.getvalue()
129 if tmpfp:
129 if tmpfp:
130 tmpfp.write(payload)
130 tmpfp.write(payload)
131 if not payload.endswith('\n'):
131 if not payload.endswith('\n'):
132 tmpfp.write('\n')
132 tmpfp.write('\n')
133 elif not diffs_seen and message and content_type == 'text/plain':
133 elif not diffs_seen and message and content_type == 'text/plain':
134 message += '\n' + payload
134 message += '\n' + payload
135 except:
135 except:
136 tmpfp.close()
136 tmpfp.close()
137 os.unlink(tmpname)
137 os.unlink(tmpname)
138 raise
138 raise
139
139
140 if subject and not message.startswith(subject):
140 if subject and not message.startswith(subject):
141 message = '%s\n%s' % (subject, message)
141 message = '%s\n%s' % (subject, message)
142 tmpfp.close()
142 tmpfp.close()
143 if not diffs_seen:
143 if not diffs_seen:
144 os.unlink(tmpname)
144 os.unlink(tmpname)
145 return None, message, user, date, branch, None, None, None
145 return None, message, user, date, branch, None, None, None
146 p1 = parents and parents.pop(0) or None
146 p1 = parents and parents.pop(0) or None
147 p2 = parents and parents.pop(0) or None
147 p2 = parents and parents.pop(0) or None
148 return tmpname, message, user, date, branch, nodeid, p1, p2
148 return tmpname, message, user, date, branch, nodeid, p1, p2
149
149
150 GP_PATCH = 1 << 0 # we have to run patch
150 GP_PATCH = 1 << 0 # we have to run patch
151 GP_FILTER = 1 << 1 # there's some copy/rename operation
151 GP_FILTER = 1 << 1 # there's some copy/rename operation
152 GP_BINARY = 1 << 2 # there's a binary patch
152 GP_BINARY = 1 << 2 # there's a binary patch
153
153
154 class patchmeta(object):
154 class patchmeta(object):
155 """Patched file metadata
155 """Patched file metadata
156
156
157 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
157 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
158 or COPY. 'path' is patched file path. 'oldpath' is set to the
158 or COPY. 'path' is patched file path. 'oldpath' is set to the
159 origin file when 'op' is either COPY or RENAME, None otherwise. If
159 origin file when 'op' is either COPY or RENAME, None otherwise. If
160 file mode is changed, 'mode' is a tuple (islink, isexec) where
160 file mode is changed, 'mode' is a tuple (islink, isexec) where
161 'islink' is True if the file is a symlink and 'isexec' is True if
161 'islink' is True if the file is a symlink and 'isexec' is True if
162 the file is executable. Otherwise, 'mode' is None.
162 the file is executable. Otherwise, 'mode' is None.
163 """
163 """
164 def __init__(self, path):
164 def __init__(self, path):
165 self.path = path
165 self.path = path
166 self.oldpath = None
166 self.oldpath = None
167 self.mode = None
167 self.mode = None
168 self.op = 'MODIFY'
168 self.op = 'MODIFY'
169 self.lineno = 0
169 self.lineno = 0
170 self.binary = False
170 self.binary = False
171
171
172 def setmode(self, mode):
172 def setmode(self, mode):
173 islink = mode & 020000
173 islink = mode & 020000
174 isexec = mode & 0100
174 isexec = mode & 0100
175 self.mode = (islink, isexec)
175 self.mode = (islink, isexec)
176
176
177 def readgitpatch(lr):
177 def readgitpatch(lr):
178 """extract git-style metadata about patches from <patchname>"""
178 """extract git-style metadata about patches from <patchname>"""
179
179
180 # Filter patch for git information
180 # Filter patch for git information
181 gp = None
181 gp = None
182 gitpatches = []
182 gitpatches = []
183 # Can have a git patch with only metadata, causing patch to complain
183 # Can have a git patch with only metadata, causing patch to complain
184 dopatch = 0
184 dopatch = 0
185
185
186 lineno = 0
186 lineno = 0
187 for line in lr:
187 for line in lr:
188 lineno += 1
188 lineno += 1
189 line = line.rstrip(' \r\n')
189 line = line.rstrip(' \r\n')
190 if line.startswith('diff --git'):
190 if line.startswith('diff --git'):
191 m = gitre.match(line)
191 m = gitre.match(line)
192 if m:
192 if m:
193 if gp:
193 if gp:
194 gitpatches.append(gp)
194 gitpatches.append(gp)
195 dst = m.group(2)
195 dst = m.group(2)
196 gp = patchmeta(dst)
196 gp = patchmeta(dst)
197 gp.lineno = lineno
197 gp.lineno = lineno
198 elif gp:
198 elif gp:
199 if line.startswith('--- '):
199 if line.startswith('--- '):
200 if gp.op in ('COPY', 'RENAME'):
200 if gp.op in ('COPY', 'RENAME'):
201 dopatch |= GP_FILTER
201 dopatch |= GP_FILTER
202 gitpatches.append(gp)
202 gitpatches.append(gp)
203 gp = None
203 gp = None
204 dopatch |= GP_PATCH
204 dopatch |= GP_PATCH
205 continue
205 continue
206 if line.startswith('rename from '):
206 if line.startswith('rename from '):
207 gp.op = 'RENAME'
207 gp.op = 'RENAME'
208 gp.oldpath = line[12:]
208 gp.oldpath = line[12:]
209 elif line.startswith('rename to '):
209 elif line.startswith('rename to '):
210 gp.path = line[10:]
210 gp.path = line[10:]
211 elif line.startswith('copy from '):
211 elif line.startswith('copy from '):
212 gp.op = 'COPY'
212 gp.op = 'COPY'
213 gp.oldpath = line[10:]
213 gp.oldpath = line[10:]
214 elif line.startswith('copy to '):
214 elif line.startswith('copy to '):
215 gp.path = line[8:]
215 gp.path = line[8:]
216 elif line.startswith('deleted file'):
216 elif line.startswith('deleted file'):
217 gp.op = 'DELETE'
217 gp.op = 'DELETE'
218 # is the deleted file a symlink?
218 # is the deleted file a symlink?
219 gp.setmode(int(line[-6:], 8))
219 gp.setmode(int(line[-6:], 8))
220 elif line.startswith('new file mode '):
220 elif line.startswith('new file mode '):
221 gp.op = 'ADD'
221 gp.op = 'ADD'
222 gp.setmode(int(line[-6:], 8))
222 gp.setmode(int(line[-6:], 8))
223 elif line.startswith('new mode '):
223 elif line.startswith('new mode '):
224 gp.setmode(int(line[-6:], 8))
224 gp.setmode(int(line[-6:], 8))
225 elif line.startswith('GIT binary patch'):
225 elif line.startswith('GIT binary patch'):
226 dopatch |= GP_BINARY
226 dopatch |= GP_BINARY
227 gp.binary = True
227 gp.binary = True
228 if gp:
228 if gp:
229 gitpatches.append(gp)
229 gitpatches.append(gp)
230
230
231 if not gitpatches:
231 if not gitpatches:
232 dopatch = GP_PATCH
232 dopatch = GP_PATCH
233
233
234 return (dopatch, gitpatches)
234 return (dopatch, gitpatches)
235
235
236 class linereader(object):
236 class linereader(object):
237 # simple class to allow pushing lines back into the input stream
237 # simple class to allow pushing lines back into the input stream
238 def __init__(self, fp, textmode=False):
238 def __init__(self, fp, textmode=False):
239 self.fp = fp
239 self.fp = fp
240 self.buf = []
240 self.buf = []
241 self.textmode = textmode
241 self.textmode = textmode
242 self.eol = None
242 self.eol = None
243
243
244 def push(self, line):
244 def push(self, line):
245 if line is not None:
245 if line is not None:
246 self.buf.append(line)
246 self.buf.append(line)
247
247
248 def readline(self):
248 def readline(self):
249 if self.buf:
249 if self.buf:
250 l = self.buf[0]
250 l = self.buf[0]
251 del self.buf[0]
251 del self.buf[0]
252 return l
252 return l
253 l = self.fp.readline()
253 l = self.fp.readline()
254 if not self.eol:
254 if not self.eol:
255 if l.endswith('\r\n'):
255 if l.endswith('\r\n'):
256 self.eol = '\r\n'
256 self.eol = '\r\n'
257 elif l.endswith('\n'):
257 elif l.endswith('\n'):
258 self.eol = '\n'
258 self.eol = '\n'
259 if self.textmode and l.endswith('\r\n'):
259 if self.textmode and l.endswith('\r\n'):
260 l = l[:-2] + '\n'
260 l = l[:-2] + '\n'
261 return l
261 return l
262
262
263 def __iter__(self):
263 def __iter__(self):
264 while 1:
264 while 1:
265 l = self.readline()
265 l = self.readline()
266 if not l:
266 if not l:
267 break
267 break
268 yield l
268 yield l
269
269
270 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
270 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
271 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
271 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
272 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
272 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
273 eolmodes = ['strict', 'crlf', 'lf', 'auto']
273 eolmodes = ['strict', 'crlf', 'lf', 'auto']
274
274
275 class patchfile(object):
275 class patchfile(object):
276 def __init__(self, ui, fname, opener, missing=False, eolmode='strict'):
276 def __init__(self, ui, fname, opener, missing=False, eolmode='strict'):
277 self.fname = fname
277 self.fname = fname
278 self.eolmode = eolmode
278 self.eolmode = eolmode
279 self.eol = None
279 self.eol = None
280 self.opener = opener
280 self.opener = opener
281 self.ui = ui
281 self.ui = ui
282 self.lines = []
282 self.lines = []
283 self.exists = False
283 self.exists = False
284 self.missing = missing
284 self.missing = missing
285 if not missing:
285 if not missing:
286 try:
286 try:
287 self.lines = self.readlines(fname)
287 self.lines = self.readlines(fname)
288 self.exists = True
288 self.exists = True
289 except IOError:
289 except IOError:
290 pass
290 pass
291 else:
291 else:
292 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
292 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
293
293
294 self.hash = {}
294 self.hash = {}
295 self.dirty = 0
295 self.dirty = 0
296 self.offset = 0
296 self.offset = 0
297 self.skew = 0
297 self.skew = 0
298 self.rej = []
298 self.rej = []
299 self.fileprinted = False
299 self.fileprinted = False
300 self.printfile(False)
300 self.printfile(False)
301 self.hunks = 0
301 self.hunks = 0
302
302
303 def readlines(self, fname):
303 def readlines(self, fname):
304 if os.path.islink(fname):
304 if os.path.islink(fname):
305 return [os.readlink(fname)]
305 return [os.readlink(fname)]
306 fp = self.opener(fname, 'r')
306 fp = self.opener(fname, 'r')
307 try:
307 try:
308 lr = linereader(fp, self.eolmode != 'strict')
308 lr = linereader(fp, self.eolmode != 'strict')
309 lines = list(lr)
309 lines = list(lr)
310 self.eol = lr.eol
310 self.eol = lr.eol
311 return lines
311 return lines
312 finally:
312 finally:
313 fp.close()
313 fp.close()
314
314
315 def writelines(self, fname, lines):
315 def writelines(self, fname, lines):
316 # Ensure supplied data ends in fname, being a regular file or
316 # Ensure supplied data ends in fname, being a regular file or
317 # a symlink. updatedir() will -too magically- take care of
317 # a symlink. updatedir() will -too magically- take care of
318 # setting it to the proper type afterwards.
318 # setting it to the proper type afterwards.
319 islink = os.path.islink(fname)
319 islink = os.path.islink(fname)
320 if islink:
320 if islink:
321 fp = cStringIO.StringIO()
321 fp = cStringIO.StringIO()
322 else:
322 else:
323 fp = self.opener(fname, 'w')
323 fp = self.opener(fname, 'w')
324 try:
324 try:
325 if self.eolmode == 'auto':
325 if self.eolmode == 'auto':
326 eol = self.eol
326 eol = self.eol
327 elif self.eolmode == 'crlf':
327 elif self.eolmode == 'crlf':
328 eol = '\r\n'
328 eol = '\r\n'
329 else:
329 else:
330 eol = '\n'
330 eol = '\n'
331
331
332 if self.eolmode != 'strict' and eol and eol != '\n':
332 if self.eolmode != 'strict' and eol and eol != '\n':
333 for l in lines:
333 for l in lines:
334 if l and l[-1] == '\n':
334 if l and l[-1] == '\n':
335 l = l[:-1] + eol
335 l = l[:-1] + eol
336 fp.write(l)
336 fp.write(l)
337 else:
337 else:
338 fp.writelines(lines)
338 fp.writelines(lines)
339 if islink:
339 if islink:
340 self.opener.symlink(fp.getvalue(), fname)
340 self.opener.symlink(fp.getvalue(), fname)
341 finally:
341 finally:
342 fp.close()
342 fp.close()
343
343
344 def unlink(self, fname):
344 def unlink(self, fname):
345 os.unlink(fname)
345 os.unlink(fname)
346
346
347 def printfile(self, warn):
347 def printfile(self, warn):
348 if self.fileprinted:
348 if self.fileprinted:
349 return
349 return
350 if warn or self.ui.verbose:
350 if warn or self.ui.verbose:
351 self.fileprinted = True
351 self.fileprinted = True
352 s = _("patching file %s\n") % self.fname
352 s = _("patching file %s\n") % self.fname
353 if warn:
353 if warn:
354 self.ui.warn(s)
354 self.ui.warn(s)
355 else:
355 else:
356 self.ui.note(s)
356 self.ui.note(s)
357
357
358
358
359 def findlines(self, l, linenum):
359 def findlines(self, l, linenum):
360 # looks through the hash and finds candidate lines. The
360 # looks through the hash and finds candidate lines. The
361 # result is a list of line numbers sorted based on distance
361 # result is a list of line numbers sorted based on distance
362 # from linenum
362 # from linenum
363
363
364 cand = self.hash.get(l, [])
364 cand = self.hash.get(l, [])
365 if len(cand) > 1:
365 if len(cand) > 1:
366 # resort our list of potentials forward then back.
366 # resort our list of potentials forward then back.
367 cand.sort(key=lambda x: abs(x - linenum))
367 cand.sort(key=lambda x: abs(x - linenum))
368 return cand
368 return cand
369
369
370 def hashlines(self):
370 def hashlines(self):
371 self.hash = {}
371 self.hash = {}
372 for x, s in enumerate(self.lines):
372 for x, s in enumerate(self.lines):
373 self.hash.setdefault(s, []).append(x)
373 self.hash.setdefault(s, []).append(x)
374
374
375 def write_rej(self):
375 def write_rej(self):
376 # our rejects are a little different from patch(1). This always
376 # our rejects are a little different from patch(1). This always
377 # creates rejects in the same form as the original patch. A file
377 # creates rejects in the same form as the original patch. A file
378 # header is inserted so that you can run the reject through patch again
378 # header is inserted so that you can run the reject through patch again
379 # without having to type the filename.
379 # without having to type the filename.
380
380
381 if not self.rej:
381 if not self.rej:
382 return
382 return
383
383
384 fname = self.fname + ".rej"
384 fname = self.fname + ".rej"
385 self.ui.warn(
385 self.ui.warn(
386 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
386 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
387 (len(self.rej), self.hunks, fname))
387 (len(self.rej), self.hunks, fname))
388
388
389 def rejlines():
389 def rejlines():
390 base = os.path.basename(self.fname)
390 base = os.path.basename(self.fname)
391 yield "--- %s\n+++ %s\n" % (base, base)
391 yield "--- %s\n+++ %s\n" % (base, base)
392 for x in self.rej:
392 for x in self.rej:
393 for l in x.hunk:
393 for l in x.hunk:
394 yield l
394 yield l
395 if l[-1] != '\n':
395 if l[-1] != '\n':
396 yield "\n\ No newline at end of file\n"
396 yield "\n\ No newline at end of file\n"
397
397
398 self.writelines(fname, rejlines())
398 self.writelines(fname, rejlines())
399
399
400 def write(self, dest=None):
400 def write(self, dest=None):
401 if not self.dirty:
401 if not self.dirty:
402 return
402 return
403 if not dest:
403 if not dest:
404 dest = self.fname
404 dest = self.fname
405 self.writelines(dest, self.lines)
405 self.writelines(dest, self.lines)
406
406
407 def close(self):
407 def close(self):
408 self.write()
408 self.write()
409 self.write_rej()
409 self.write_rej()
410
410
411 def apply(self, h):
411 def apply(self, h):
412 if not h.complete():
412 if not h.complete():
413 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
413 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
414 (h.number, h.desc, len(h.a), h.lena, len(h.b),
414 (h.number, h.desc, len(h.a), h.lena, len(h.b),
415 h.lenb))
415 h.lenb))
416
416
417 self.hunks += 1
417 self.hunks += 1
418
418
419 if self.missing:
419 if self.missing:
420 self.rej.append(h)
420 self.rej.append(h)
421 return -1
421 return -1
422
422
423 if self.exists and h.createfile():
423 if self.exists and h.createfile():
424 self.ui.warn(_("file %s already exists\n") % self.fname)
424 self.ui.warn(_("file %s already exists\n") % self.fname)
425 self.rej.append(h)
425 self.rej.append(h)
426 return -1
426 return -1
427
427
428 if isinstance(h, binhunk):
428 if isinstance(h, binhunk):
429 if h.rmfile():
429 if h.rmfile():
430 self.unlink(self.fname)
430 self.unlink(self.fname)
431 else:
431 else:
432 self.lines[:] = h.new()
432 self.lines[:] = h.new()
433 self.offset += len(h.new())
433 self.offset += len(h.new())
434 self.dirty = 1
434 self.dirty = 1
435 return 0
435 return 0
436
436
437 horig = h
437 horig = h
438 if (self.eolmode in ('crlf', 'lf')
438 if (self.eolmode in ('crlf', 'lf')
439 or self.eolmode == 'auto' and self.eol):
439 or self.eolmode == 'auto' and self.eol):
440 # If new eols are going to be normalized, then normalize
440 # If new eols are going to be normalized, then normalize
441 # hunk data before patching. Otherwise, preserve input
441 # hunk data before patching. Otherwise, preserve input
442 # line-endings.
442 # line-endings.
443 h = h.getnormalized()
443 h = h.getnormalized()
444
444
445 # fast case first, no offsets, no fuzz
445 # fast case first, no offsets, no fuzz
446 old = h.old()
446 old = h.old()
447 # patch starts counting at 1 unless we are adding the file
447 # patch starts counting at 1 unless we are adding the file
448 if h.starta == 0:
448 if h.starta == 0:
449 start = 0
449 start = 0
450 else:
450 else:
451 start = h.starta + self.offset - 1
451 start = h.starta + self.offset - 1
452 orig_start = start
452 orig_start = start
453 # if there's skew we want to emit the "(offset %d lines)" even
453 # if there's skew we want to emit the "(offset %d lines)" even
454 # when the hunk cleanly applies at start + skew, so skip the
454 # when the hunk cleanly applies at start + skew, so skip the
455 # fast case code
455 # fast case code
456 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
456 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
457 if h.rmfile():
457 if h.rmfile():
458 self.unlink(self.fname)
458 self.unlink(self.fname)
459 else:
459 else:
460 self.lines[start : start + h.lena] = h.new()
460 self.lines[start : start + h.lena] = h.new()
461 self.offset += h.lenb - h.lena
461 self.offset += h.lenb - h.lena
462 self.dirty = 1
462 self.dirty = 1
463 return 0
463 return 0
464
464
465 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
465 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
466 self.hashlines()
466 self.hashlines()
467 if h.hunk[-1][0] != ' ':
467 if h.hunk[-1][0] != ' ':
468 # if the hunk tried to put something at the bottom of the file
468 # if the hunk tried to put something at the bottom of the file
469 # override the start line and use eof here
469 # override the start line and use eof here
470 search_start = len(self.lines)
470 search_start = len(self.lines)
471 else:
471 else:
472 search_start = orig_start + self.skew
472 search_start = orig_start + self.skew
473
473
474 for fuzzlen in xrange(3):
474 for fuzzlen in xrange(3):
475 for toponly in [ True, False ]:
475 for toponly in [ True, False ]:
476 old = h.old(fuzzlen, toponly)
476 old = h.old(fuzzlen, toponly)
477
477
478 cand = self.findlines(old[0][1:], search_start)
478 cand = self.findlines(old[0][1:], search_start)
479 for l in cand:
479 for l in cand:
480 if diffhelpers.testhunk(old, self.lines, l) == 0:
480 if diffhelpers.testhunk(old, self.lines, l) == 0:
481 newlines = h.new(fuzzlen, toponly)
481 newlines = h.new(fuzzlen, toponly)
482 self.lines[l : l + len(old)] = newlines
482 self.lines[l : l + len(old)] = newlines
483 self.offset += len(newlines) - len(old)
483 self.offset += len(newlines) - len(old)
484 self.skew = l - orig_start
484 self.skew = l - orig_start
485 self.dirty = 1
485 self.dirty = 1
486 if fuzzlen:
486 if fuzzlen:
487 fuzzstr = "with fuzz %d " % fuzzlen
487 fuzzstr = "with fuzz %d " % fuzzlen
488 f = self.ui.warn
488 f = self.ui.warn
489 self.printfile(True)
489 self.printfile(True)
490 else:
490 else:
491 fuzzstr = ""
491 fuzzstr = ""
492 f = self.ui.note
492 f = self.ui.note
493 offset = l - orig_start - fuzzlen
493 offset = l - orig_start - fuzzlen
494 if offset == 1:
494 if offset == 1:
495 msg = _("Hunk #%d succeeded at %d %s"
495 msg = _("Hunk #%d succeeded at %d %s"
496 "(offset %d line).\n")
496 "(offset %d line).\n")
497 else:
497 else:
498 msg = _("Hunk #%d succeeded at %d %s"
498 msg = _("Hunk #%d succeeded at %d %s"
499 "(offset %d lines).\n")
499 "(offset %d lines).\n")
500 f(msg % (h.number, l+1, fuzzstr, offset))
500 f(msg % (h.number, l+1, fuzzstr, offset))
501 return fuzzlen
501 return fuzzlen
502 self.printfile(True)
502 self.printfile(True)
503 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
503 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
504 self.rej.append(horig)
504 self.rej.append(horig)
505 return -1
505 return -1
506
506
507 class hunk(object):
507 class hunk(object):
508 def __init__(self, desc, num, lr, context, create=False, remove=False):
508 def __init__(self, desc, num, lr, context, create=False, remove=False):
509 self.number = num
509 self.number = num
510 self.desc = desc
510 self.desc = desc
511 self.hunk = [ desc ]
511 self.hunk = [ desc ]
512 self.a = []
512 self.a = []
513 self.b = []
513 self.b = []
514 self.starta = self.lena = None
514 self.starta = self.lena = None
515 self.startb = self.lenb = None
515 self.startb = self.lenb = None
516 if lr is not None:
516 if lr is not None:
517 if context:
517 if context:
518 self.read_context_hunk(lr)
518 self.read_context_hunk(lr)
519 else:
519 else:
520 self.read_unified_hunk(lr)
520 self.read_unified_hunk(lr)
521 self.create = create
521 self.create = create
522 self.remove = remove and not create
522 self.remove = remove and not create
523
523
524 def getnormalized(self):
524 def getnormalized(self):
525 """Return a copy with line endings normalized to LF."""
525 """Return a copy with line endings normalized to LF."""
526
526
527 def normalize(lines):
527 def normalize(lines):
528 nlines = []
528 nlines = []
529 for line in lines:
529 for line in lines:
530 if line.endswith('\r\n'):
530 if line.endswith('\r\n'):
531 line = line[:-2] + '\n'
531 line = line[:-2] + '\n'
532 nlines.append(line)
532 nlines.append(line)
533 return nlines
533 return nlines
534
534
535 # Dummy object, it is rebuilt manually
535 # Dummy object, it is rebuilt manually
536 nh = hunk(self.desc, self.number, None, None, False, False)
536 nh = hunk(self.desc, self.number, None, None, False, False)
537 nh.number = self.number
537 nh.number = self.number
538 nh.desc = self.desc
538 nh.desc = self.desc
539 nh.a = normalize(self.a)
539 nh.a = normalize(self.a)
540 nh.b = normalize(self.b)
540 nh.b = normalize(self.b)
541 nh.starta = self.starta
541 nh.starta = self.starta
542 nh.startb = self.startb
542 nh.startb = self.startb
543 nh.lena = self.lena
543 nh.lena = self.lena
544 nh.lenb = self.lenb
544 nh.lenb = self.lenb
545 nh.create = self.create
545 nh.create = self.create
546 nh.remove = self.remove
546 nh.remove = self.remove
547 return nh
547 return nh
548
548
549 def read_unified_hunk(self, lr):
549 def read_unified_hunk(self, lr):
550 m = unidesc.match(self.desc)
550 m = unidesc.match(self.desc)
551 if not m:
551 if not m:
552 raise PatchError(_("bad hunk #%d") % self.number)
552 raise PatchError(_("bad hunk #%d") % self.number)
553 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
553 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
554 if self.lena is None:
554 if self.lena is None:
555 self.lena = 1
555 self.lena = 1
556 else:
556 else:
557 self.lena = int(self.lena)
557 self.lena = int(self.lena)
558 if self.lenb is None:
558 if self.lenb is None:
559 self.lenb = 1
559 self.lenb = 1
560 else:
560 else:
561 self.lenb = int(self.lenb)
561 self.lenb = int(self.lenb)
562 self.starta = int(self.starta)
562 self.starta = int(self.starta)
563 self.startb = int(self.startb)
563 self.startb = int(self.startb)
564 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
564 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
565 # if we hit eof before finishing out the hunk, the last line will
565 # if we hit eof before finishing out the hunk, the last line will
566 # be zero length. Lets try to fix it up.
566 # be zero length. Lets try to fix it up.
567 while len(self.hunk[-1]) == 0:
567 while len(self.hunk[-1]) == 0:
568 del self.hunk[-1]
568 del self.hunk[-1]
569 del self.a[-1]
569 del self.a[-1]
570 del self.b[-1]
570 del self.b[-1]
571 self.lena -= 1
571 self.lena -= 1
572 self.lenb -= 1
572 self.lenb -= 1
573
573
574 def read_context_hunk(self, lr):
574 def read_context_hunk(self, lr):
575 self.desc = lr.readline()
575 self.desc = lr.readline()
576 m = contextdesc.match(self.desc)
576 m = contextdesc.match(self.desc)
577 if not m:
577 if not m:
578 raise PatchError(_("bad hunk #%d") % self.number)
578 raise PatchError(_("bad hunk #%d") % self.number)
579 foo, self.starta, foo2, aend, foo3 = m.groups()
579 foo, self.starta, foo2, aend, foo3 = m.groups()
580 self.starta = int(self.starta)
580 self.starta = int(self.starta)
581 if aend is None:
581 if aend is None:
582 aend = self.starta
582 aend = self.starta
583 self.lena = int(aend) - self.starta
583 self.lena = int(aend) - self.starta
584 if self.starta:
584 if self.starta:
585 self.lena += 1
585 self.lena += 1
586 for x in xrange(self.lena):
586 for x in xrange(self.lena):
587 l = lr.readline()
587 l = lr.readline()
588 if l.startswith('---'):
588 if l.startswith('---'):
589 lr.push(l)
589 lr.push(l)
590 break
590 break
591 s = l[2:]
591 s = l[2:]
592 if l.startswith('- ') or l.startswith('! '):
592 if l.startswith('- ') or l.startswith('! '):
593 u = '-' + s
593 u = '-' + s
594 elif l.startswith(' '):
594 elif l.startswith(' '):
595 u = ' ' + s
595 u = ' ' + s
596 else:
596 else:
597 raise PatchError(_("bad hunk #%d old text line %d") %
597 raise PatchError(_("bad hunk #%d old text line %d") %
598 (self.number, x))
598 (self.number, x))
599 self.a.append(u)
599 self.a.append(u)
600 self.hunk.append(u)
600 self.hunk.append(u)
601
601
602 l = lr.readline()
602 l = lr.readline()
603 if l.startswith('\ '):
603 if l.startswith('\ '):
604 s = self.a[-1][:-1]
604 s = self.a[-1][:-1]
605 self.a[-1] = s
605 self.a[-1] = s
606 self.hunk[-1] = s
606 self.hunk[-1] = s
607 l = lr.readline()
607 l = lr.readline()
608 m = contextdesc.match(l)
608 m = contextdesc.match(l)
609 if not m:
609 if not m:
610 raise PatchError(_("bad hunk #%d") % self.number)
610 raise PatchError(_("bad hunk #%d") % self.number)
611 foo, self.startb, foo2, bend, foo3 = m.groups()
611 foo, self.startb, foo2, bend, foo3 = m.groups()
612 self.startb = int(self.startb)
612 self.startb = int(self.startb)
613 if bend is None:
613 if bend is None:
614 bend = self.startb
614 bend = self.startb
615 self.lenb = int(bend) - self.startb
615 self.lenb = int(bend) - self.startb
616 if self.startb:
616 if self.startb:
617 self.lenb += 1
617 self.lenb += 1
618 hunki = 1
618 hunki = 1
619 for x in xrange(self.lenb):
619 for x in xrange(self.lenb):
620 l = lr.readline()
620 l = lr.readline()
621 if l.startswith('\ '):
621 if l.startswith('\ '):
622 s = self.b[-1][:-1]
622 s = self.b[-1][:-1]
623 self.b[-1] = s
623 self.b[-1] = s
624 self.hunk[hunki-1] = s
624 self.hunk[hunki-1] = s
625 continue
625 continue
626 if not l:
626 if not l:
627 lr.push(l)
627 lr.push(l)
628 break
628 break
629 s = l[2:]
629 s = l[2:]
630 if l.startswith('+ ') or l.startswith('! '):
630 if l.startswith('+ ') or l.startswith('! '):
631 u = '+' + s
631 u = '+' + s
632 elif l.startswith(' '):
632 elif l.startswith(' '):
633 u = ' ' + s
633 u = ' ' + s
634 elif len(self.b) == 0:
634 elif len(self.b) == 0:
635 # this can happen when the hunk does not add any lines
635 # this can happen when the hunk does not add any lines
636 lr.push(l)
636 lr.push(l)
637 break
637 break
638 else:
638 else:
639 raise PatchError(_("bad hunk #%d old text line %d") %
639 raise PatchError(_("bad hunk #%d old text line %d") %
640 (self.number, x))
640 (self.number, x))
641 self.b.append(s)
641 self.b.append(s)
642 while True:
642 while True:
643 if hunki >= len(self.hunk):
643 if hunki >= len(self.hunk):
644 h = ""
644 h = ""
645 else:
645 else:
646 h = self.hunk[hunki]
646 h = self.hunk[hunki]
647 hunki += 1
647 hunki += 1
648 if h == u:
648 if h == u:
649 break
649 break
650 elif h.startswith('-'):
650 elif h.startswith('-'):
651 continue
651 continue
652 else:
652 else:
653 self.hunk.insert(hunki-1, u)
653 self.hunk.insert(hunki-1, u)
654 break
654 break
655
655
656 if not self.a:
656 if not self.a:
657 # this happens when lines were only added to the hunk
657 # this happens when lines were only added to the hunk
658 for x in self.hunk:
658 for x in self.hunk:
659 if x.startswith('-') or x.startswith(' '):
659 if x.startswith('-') or x.startswith(' '):
660 self.a.append(x)
660 self.a.append(x)
661 if not self.b:
661 if not self.b:
662 # this happens when lines were only deleted from the hunk
662 # this happens when lines were only deleted from the hunk
663 for x in self.hunk:
663 for x in self.hunk:
664 if x.startswith('+') or x.startswith(' '):
664 if x.startswith('+') or x.startswith(' '):
665 self.b.append(x[1:])
665 self.b.append(x[1:])
666 # @@ -start,len +start,len @@
666 # @@ -start,len +start,len @@
667 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
667 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
668 self.startb, self.lenb)
668 self.startb, self.lenb)
669 self.hunk[0] = self.desc
669 self.hunk[0] = self.desc
670
670
671 def fix_newline(self):
671 def fix_newline(self):
672 diffhelpers.fix_newline(self.hunk, self.a, self.b)
672 diffhelpers.fix_newline(self.hunk, self.a, self.b)
673
673
674 def complete(self):
674 def complete(self):
675 return len(self.a) == self.lena and len(self.b) == self.lenb
675 return len(self.a) == self.lena and len(self.b) == self.lenb
676
676
677 def createfile(self):
677 def createfile(self):
678 return self.starta == 0 and self.lena == 0 and self.create
678 return self.starta == 0 and self.lena == 0 and self.create
679
679
680 def rmfile(self):
680 def rmfile(self):
681 return self.startb == 0 and self.lenb == 0 and self.remove
681 return self.startb == 0 and self.lenb == 0 and self.remove
682
682
683 def fuzzit(self, l, fuzz, toponly):
683 def fuzzit(self, l, fuzz, toponly):
684 # this removes context lines from the top and bottom of list 'l'. It
684 # this removes context lines from the top and bottom of list 'l'. It
685 # checks the hunk to make sure only context lines are removed, and then
685 # checks the hunk to make sure only context lines are removed, and then
686 # returns a new shortened list of lines.
686 # returns a new shortened list of lines.
687 fuzz = min(fuzz, len(l)-1)
687 fuzz = min(fuzz, len(l)-1)
688 if fuzz:
688 if fuzz:
689 top = 0
689 top = 0
690 bot = 0
690 bot = 0
691 hlen = len(self.hunk)
691 hlen = len(self.hunk)
692 for x in xrange(hlen-1):
692 for x in xrange(hlen-1):
693 # the hunk starts with the @@ line, so use x+1
693 # the hunk starts with the @@ line, so use x+1
694 if self.hunk[x+1][0] == ' ':
694 if self.hunk[x+1][0] == ' ':
695 top += 1
695 top += 1
696 else:
696 else:
697 break
697 break
698 if not toponly:
698 if not toponly:
699 for x in xrange(hlen-1):
699 for x in xrange(hlen-1):
700 if self.hunk[hlen-bot-1][0] == ' ':
700 if self.hunk[hlen-bot-1][0] == ' ':
701 bot += 1
701 bot += 1
702 else:
702 else:
703 break
703 break
704
704
705 # top and bot now count context in the hunk
705 # top and bot now count context in the hunk
706 # adjust them if either one is short
706 # adjust them if either one is short
707 context = max(top, bot, 3)
707 context = max(top, bot, 3)
708 if bot < context:
708 if bot < context:
709 bot = max(0, fuzz - (context - bot))
709 bot = max(0, fuzz - (context - bot))
710 else:
710 else:
711 bot = min(fuzz, bot)
711 bot = min(fuzz, bot)
712 if top < context:
712 if top < context:
713 top = max(0, fuzz - (context - top))
713 top = max(0, fuzz - (context - top))
714 else:
714 else:
715 top = min(fuzz, top)
715 top = min(fuzz, top)
716
716
717 return l[top:len(l)-bot]
717 return l[top:len(l)-bot]
718 return l
718 return l
719
719
720 def old(self, fuzz=0, toponly=False):
720 def old(self, fuzz=0, toponly=False):
721 return self.fuzzit(self.a, fuzz, toponly)
721 return self.fuzzit(self.a, fuzz, toponly)
722
722
723 def new(self, fuzz=0, toponly=False):
723 def new(self, fuzz=0, toponly=False):
724 return self.fuzzit(self.b, fuzz, toponly)
724 return self.fuzzit(self.b, fuzz, toponly)
725
725
726 class binhunk:
726 class binhunk:
727 'A binary patch file. Only understands literals so far.'
727 'A binary patch file. Only understands literals so far.'
728 def __init__(self, gitpatch):
728 def __init__(self, gitpatch):
729 self.gitpatch = gitpatch
729 self.gitpatch = gitpatch
730 self.text = None
730 self.text = None
731 self.hunk = ['GIT binary patch\n']
731 self.hunk = ['GIT binary patch\n']
732
732
733 def createfile(self):
733 def createfile(self):
734 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
734 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
735
735
736 def rmfile(self):
736 def rmfile(self):
737 return self.gitpatch.op == 'DELETE'
737 return self.gitpatch.op == 'DELETE'
738
738
739 def complete(self):
739 def complete(self):
740 return self.text is not None
740 return self.text is not None
741
741
742 def new(self):
742 def new(self):
743 return [self.text]
743 return [self.text]
744
744
745 def extract(self, lr):
745 def extract(self, lr):
746 line = lr.readline()
746 line = lr.readline()
747 self.hunk.append(line)
747 self.hunk.append(line)
748 while line and not line.startswith('literal '):
748 while line and not line.startswith('literal '):
749 line = lr.readline()
749 line = lr.readline()
750 self.hunk.append(line)
750 self.hunk.append(line)
751 if not line:
751 if not line:
752 raise PatchError(_('could not extract binary patch'))
752 raise PatchError(_('could not extract binary patch'))
753 size = int(line[8:].rstrip())
753 size = int(line[8:].rstrip())
754 dec = []
754 dec = []
755 line = lr.readline()
755 line = lr.readline()
756 self.hunk.append(line)
756 self.hunk.append(line)
757 while len(line) > 1:
757 while len(line) > 1:
758 l = line[0]
758 l = line[0]
759 if l <= 'Z' and l >= 'A':
759 if l <= 'Z' and l >= 'A':
760 l = ord(l) - ord('A') + 1
760 l = ord(l) - ord('A') + 1
761 else:
761 else:
762 l = ord(l) - ord('a') + 27
762 l = ord(l) - ord('a') + 27
763 dec.append(base85.b85decode(line[1:-1])[:l])
763 dec.append(base85.b85decode(line[1:-1])[:l])
764 line = lr.readline()
764 line = lr.readline()
765 self.hunk.append(line)
765 self.hunk.append(line)
766 text = zlib.decompress(''.join(dec))
766 text = zlib.decompress(''.join(dec))
767 if len(text) != size:
767 if len(text) != size:
768 raise PatchError(_('binary patch is %d bytes, not %d') %
768 raise PatchError(_('binary patch is %d bytes, not %d') %
769 len(text), size)
769 len(text), size)
770 self.text = text
770 self.text = text
771
771
772 def parsefilename(str):
772 def parsefilename(str):
773 # --- filename \t|space stuff
773 # --- filename \t|space stuff
774 s = str[4:].rstrip('\r\n')
774 s = str[4:].rstrip('\r\n')
775 i = s.find('\t')
775 i = s.find('\t')
776 if i < 0:
776 if i < 0:
777 i = s.find(' ')
777 i = s.find(' ')
778 if i < 0:
778 if i < 0:
779 return s
779 return s
780 return s[:i]
780 return s[:i]
781
781
782 def selectfile(afile_orig, bfile_orig, hunk, strip):
782 def selectfile(afile_orig, bfile_orig, hunk, strip):
783 def pathstrip(path, count=1):
783 def pathstrip(path, count=1):
784 pathlen = len(path)
784 pathlen = len(path)
785 i = 0
785 i = 0
786 if count == 0:
786 if count == 0:
787 return '', path.rstrip()
787 return '', path.rstrip()
788 while count > 0:
788 while count > 0:
789 i = path.find('/', i)
789 i = path.find('/', i)
790 if i == -1:
790 if i == -1:
791 raise PatchError(_("unable to strip away %d dirs from %s") %
791 raise PatchError(_("unable to strip away %d dirs from %s") %
792 (count, path))
792 (count, path))
793 i += 1
793 i += 1
794 # consume '//' in the path
794 # consume '//' in the path
795 while i < pathlen - 1 and path[i] == '/':
795 while i < pathlen - 1 and path[i] == '/':
796 i += 1
796 i += 1
797 count -= 1
797 count -= 1
798 return path[:i].lstrip(), path[i:].rstrip()
798 return path[:i].lstrip(), path[i:].rstrip()
799
799
800 nulla = afile_orig == "/dev/null"
800 nulla = afile_orig == "/dev/null"
801 nullb = bfile_orig == "/dev/null"
801 nullb = bfile_orig == "/dev/null"
802 abase, afile = pathstrip(afile_orig, strip)
802 abase, afile = pathstrip(afile_orig, strip)
803 gooda = not nulla and util.lexists(afile)
803 gooda = not nulla and util.lexists(afile)
804 bbase, bfile = pathstrip(bfile_orig, strip)
804 bbase, bfile = pathstrip(bfile_orig, strip)
805 if afile == bfile:
805 if afile == bfile:
806 goodb = gooda
806 goodb = gooda
807 else:
807 else:
808 goodb = not nullb and os.path.exists(bfile)
808 goodb = not nullb and os.path.exists(bfile)
809 createfunc = hunk.createfile
809 createfunc = hunk.createfile
810 missing = not goodb and not gooda and not createfunc()
810 missing = not goodb and not gooda and not createfunc()
811
811
812 # some diff programs apparently produce create patches where the
812 # some diff programs apparently produce create patches where the
813 # afile is not /dev/null, but rather the same name as the bfile
813 # afile is not /dev/null, but rather the same name as the bfile
814 if missing and afile == bfile:
814 if missing and afile == bfile:
815 # this isn't very pretty
815 # this isn't very pretty
816 hunk.create = True
816 hunk.create = True
817 if createfunc():
817 if createfunc():
818 missing = False
818 missing = False
819 else:
819 else:
820 hunk.create = False
820 hunk.create = False
821
821
822 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
822 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
823 # diff is between a file and its backup. In this case, the original
823 # diff is between a file and its backup. In this case, the original
824 # file should be patched (see original mpatch code).
824 # file should be patched (see original mpatch code).
825 isbackup = (abase == bbase and bfile.startswith(afile))
825 isbackup = (abase == bbase and bfile.startswith(afile))
826 fname = None
826 fname = None
827 if not missing:
827 if not missing:
828 if gooda and goodb:
828 if gooda and goodb:
829 fname = isbackup and afile or bfile
829 fname = isbackup and afile or bfile
830 elif gooda:
830 elif gooda:
831 fname = afile
831 fname = afile
832
832
833 if not fname:
833 if not fname:
834 if not nullb:
834 if not nullb:
835 fname = isbackup and afile or bfile
835 fname = isbackup and afile or bfile
836 elif not nulla:
836 elif not nulla:
837 fname = afile
837 fname = afile
838 else:
838 else:
839 raise PatchError(_("undefined source and destination files"))
839 raise PatchError(_("undefined source and destination files"))
840
840
841 return fname, missing
841 return fname, missing
842
842
843 def scangitpatch(lr, firstline):
843 def scangitpatch(lr, firstline):
844 """
844 """
845 Git patches can emit:
845 Git patches can emit:
846 - rename a to b
846 - rename a to b
847 - change b
847 - change b
848 - copy a to c
848 - copy a to c
849 - change c
849 - change c
850
850
851 We cannot apply this sequence as-is, the renamed 'a' could not be
851 We cannot apply this sequence as-is, the renamed 'a' could not be
852 found for it would have been renamed already. And we cannot copy
852 found for it would have been renamed already. And we cannot copy
853 from 'b' instead because 'b' would have been changed already. So
853 from 'b' instead because 'b' would have been changed already. So
854 we scan the git patch for copy and rename commands so we can
854 we scan the git patch for copy and rename commands so we can
855 perform the copies ahead of time.
855 perform the copies ahead of time.
856 """
856 """
857 pos = 0
857 pos = 0
858 try:
858 try:
859 pos = lr.fp.tell()
859 pos = lr.fp.tell()
860 fp = lr.fp
860 fp = lr.fp
861 except IOError:
861 except IOError:
862 fp = cStringIO.StringIO(lr.fp.read())
862 fp = cStringIO.StringIO(lr.fp.read())
863 gitlr = linereader(fp, lr.textmode)
863 gitlr = linereader(fp, lr.textmode)
864 gitlr.push(firstline)
864 gitlr.push(firstline)
865 (dopatch, gitpatches) = readgitpatch(gitlr)
865 (dopatch, gitpatches) = readgitpatch(gitlr)
866 fp.seek(pos)
866 fp.seek(pos)
867 return dopatch, gitpatches
867 return dopatch, gitpatches
868
868
869 def iterhunks(ui, fp, sourcefile=None):
869 def iterhunks(ui, fp, sourcefile=None):
870 """Read a patch and yield the following events:
870 """Read a patch and yield the following events:
871 - ("file", afile, bfile, firsthunk): select a new target file.
871 - ("file", afile, bfile, firsthunk): select a new target file.
872 - ("hunk", hunk): a new hunk is ready to be applied, follows a
872 - ("hunk", hunk): a new hunk is ready to be applied, follows a
873 "file" event.
873 "file" event.
874 - ("git", gitchanges): current diff is in git format, gitchanges
874 - ("git", gitchanges): current diff is in git format, gitchanges
875 maps filenames to gitpatch records. Unique event.
875 maps filenames to gitpatch records. Unique event.
876 """
876 """
877 changed = {}
877 changed = {}
878 current_hunk = None
878 current_hunk = None
879 afile = ""
879 afile = ""
880 bfile = ""
880 bfile = ""
881 state = None
881 state = None
882 hunknum = 0
882 hunknum = 0
883 emitfile = False
883 emitfile = False
884 git = False
884 git = False
885
885
886 # our states
886 # our states
887 BFILE = 1
887 BFILE = 1
888 context = None
888 context = None
889 lr = linereader(fp)
889 lr = linereader(fp)
890 dopatch = True
890 dopatch = True
891 # gitworkdone is True if a git operation (copy, rename, ...) was
891 # gitworkdone is True if a git operation (copy, rename, ...) was
892 # performed already for the current file. Useful when the file
892 # performed already for the current file. Useful when the file
893 # section may have no hunk.
893 # section may have no hunk.
894 gitworkdone = False
894 gitworkdone = False
895
895
896 while True:
896 while True:
897 newfile = False
897 newfile = False
898 x = lr.readline()
898 x = lr.readline()
899 if not x:
899 if not x:
900 break
900 break
901 if current_hunk:
901 if current_hunk:
902 if x.startswith('\ '):
902 if x.startswith('\ '):
903 current_hunk.fix_newline()
903 current_hunk.fix_newline()
904 yield 'hunk', current_hunk
904 yield 'hunk', current_hunk
905 current_hunk = None
905 current_hunk = None
906 gitworkdone = False
906 gitworkdone = False
907 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
907 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
908 ((context is not False) and x.startswith('***************')))):
908 ((context is not False) and x.startswith('***************')))):
909 try:
909 try:
910 if context is None and x.startswith('***************'):
910 if context is None and x.startswith('***************'):
911 context = True
911 context = True
912 gpatch = changed.get(bfile)
912 gpatch = changed.get(bfile)
913 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
913 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
914 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
914 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
915 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
915 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
916 except PatchError, err:
916 except PatchError, err:
917 ui.debug(err)
917 ui.debug(err)
918 current_hunk = None
918 current_hunk = None
919 continue
919 continue
920 hunknum += 1
920 hunknum += 1
921 if emitfile:
921 if emitfile:
922 emitfile = False
922 emitfile = False
923 yield 'file', (afile, bfile, current_hunk)
923 yield 'file', (afile, bfile, current_hunk)
924 elif state == BFILE and x.startswith('GIT binary patch'):
924 elif state == BFILE and x.startswith('GIT binary patch'):
925 current_hunk = binhunk(changed[bfile])
925 current_hunk = binhunk(changed[bfile])
926 hunknum += 1
926 hunknum += 1
927 if emitfile:
927 if emitfile:
928 emitfile = False
928 emitfile = False
929 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
929 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
930 current_hunk.extract(lr)
930 current_hunk.extract(lr)
931 elif x.startswith('diff --git'):
931 elif x.startswith('diff --git'):
932 # check for git diff, scanning the whole patch file if needed
932 # check for git diff, scanning the whole patch file if needed
933 m = gitre.match(x)
933 m = gitre.match(x)
934 if m:
934 if m:
935 afile, bfile = m.group(1, 2)
935 afile, bfile = m.group(1, 2)
936 if not git:
936 if not git:
937 git = True
937 git = True
938 dopatch, gitpatches = scangitpatch(lr, x)
938 dopatch, gitpatches = scangitpatch(lr, x)
939 yield 'git', gitpatches
939 yield 'git', gitpatches
940 for gp in gitpatches:
940 for gp in gitpatches:
941 changed[gp.path] = gp
941 changed[gp.path] = gp
942 # else error?
942 # else error?
943 # copy/rename + modify should modify target, not source
943 # copy/rename + modify should modify target, not source
944 gp = changed.get(bfile)
944 gp = changed.get(bfile)
945 if gp and gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD'):
945 if gp and gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD'):
946 afile = bfile
946 afile = bfile
947 gitworkdone = True
947 gitworkdone = True
948 newfile = True
948 newfile = True
949 elif x.startswith('---'):
949 elif x.startswith('---'):
950 # check for a unified diff
950 # check for a unified diff
951 l2 = lr.readline()
951 l2 = lr.readline()
952 if not l2.startswith('+++'):
952 if not l2.startswith('+++'):
953 lr.push(l2)
953 lr.push(l2)
954 continue
954 continue
955 newfile = True
955 newfile = True
956 context = False
956 context = False
957 afile = parsefilename(x)
957 afile = parsefilename(x)
958 bfile = parsefilename(l2)
958 bfile = parsefilename(l2)
959 elif x.startswith('***'):
959 elif x.startswith('***'):
960 # check for a context diff
960 # check for a context diff
961 l2 = lr.readline()
961 l2 = lr.readline()
962 if not l2.startswith('---'):
962 if not l2.startswith('---'):
963 lr.push(l2)
963 lr.push(l2)
964 continue
964 continue
965 l3 = lr.readline()
965 l3 = lr.readline()
966 lr.push(l3)
966 lr.push(l3)
967 if not l3.startswith("***************"):
967 if not l3.startswith("***************"):
968 lr.push(l2)
968 lr.push(l2)
969 continue
969 continue
970 newfile = True
970 newfile = True
971 context = True
971 context = True
972 afile = parsefilename(x)
972 afile = parsefilename(x)
973 bfile = parsefilename(l2)
973 bfile = parsefilename(l2)
974
974
975 if newfile:
975 if newfile:
976 emitfile = True
976 emitfile = True
977 state = BFILE
977 state = BFILE
978 hunknum = 0
978 hunknum = 0
979 if current_hunk:
979 if current_hunk:
980 if current_hunk.complete():
980 if current_hunk.complete():
981 yield 'hunk', current_hunk
981 yield 'hunk', current_hunk
982 else:
982 else:
983 raise PatchError(_("malformed patch %s %s") % (afile,
983 raise PatchError(_("malformed patch %s %s") % (afile,
984 current_hunk.desc))
984 current_hunk.desc))
985
985
986 if hunknum == 0 and dopatch and not gitworkdone:
986 if hunknum == 0 and dopatch and not gitworkdone:
987 raise NoHunks
987 raise NoHunks
988
988
989 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
989 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
990 """
990 """
991 Reads a patch from fp and tries to apply it.
991 Reads a patch from fp and tries to apply it.
992
992
993 The dict 'changed' is filled in with all of the filenames changed
993 The dict 'changed' is filled in with all of the filenames changed
994 by the patch. Returns 0 for a clean patch, -1 if any rejects were
994 by the patch. Returns 0 for a clean patch, -1 if any rejects were
995 found and 1 if there was any fuzz.
995 found and 1 if there was any fuzz.
996
996
997 If 'eolmode' is 'strict', the patch content and patched file are
997 If 'eolmode' is 'strict', the patch content and patched file are
998 read in binary mode. Otherwise, line endings are ignored when
998 read in binary mode. Otherwise, line endings are ignored when
999 patching then normalized according to 'eolmode'.
999 patching then normalized according to 'eolmode'.
1000 """
1000 """
1001 rejects = 0
1001 rejects = 0
1002 err = 0
1002 err = 0
1003 current_file = None
1003 current_file = None
1004 gitpatches = None
1004 gitpatches = None
1005 opener = util.opener(os.getcwd())
1005 opener = util.opener(os.getcwd())
1006
1006
1007 def closefile():
1007 def closefile():
1008 if not current_file:
1008 if not current_file:
1009 return 0
1009 return 0
1010 current_file.close()
1010 current_file.close()
1011 return len(current_file.rej)
1011 return len(current_file.rej)
1012
1012
1013 for state, values in iterhunks(ui, fp, sourcefile):
1013 for state, values in iterhunks(ui, fp, sourcefile):
1014 if state == 'hunk':
1014 if state == 'hunk':
1015 if not current_file:
1015 if not current_file:
1016 continue
1016 continue
1017 current_hunk = values
1017 current_hunk = values
1018 ret = current_file.apply(current_hunk)
1018 ret = current_file.apply(current_hunk)
1019 if ret >= 0:
1019 if ret >= 0:
1020 changed.setdefault(current_file.fname, None)
1020 changed.setdefault(current_file.fname, None)
1021 if ret > 0:
1021 if ret > 0:
1022 err = 1
1022 err = 1
1023 elif state == 'file':
1023 elif state == 'file':
1024 rejects += closefile()
1024 rejects += closefile()
1025 afile, bfile, first_hunk = values
1025 afile, bfile, first_hunk = values
1026 try:
1026 try:
1027 if sourcefile:
1027 if sourcefile:
1028 current_file = patchfile(ui, sourcefile, opener, eolmode=eolmode)
1028 current_file = patchfile(ui, sourcefile, opener, eolmode=eolmode)
1029 else:
1029 else:
1030 current_file, missing = selectfile(afile, bfile, first_hunk,
1030 current_file, missing = selectfile(afile, bfile, first_hunk,
1031 strip)
1031 strip)
1032 current_file = patchfile(ui, current_file, opener, missing, eolmode)
1032 current_file = patchfile(ui, current_file, opener, missing, eolmode)
1033 except PatchError, err:
1033 except PatchError, err:
1034 ui.warn(str(err) + '\n')
1034 ui.warn(str(err) + '\n')
1035 current_file, current_hunk = None, None
1035 current_file, current_hunk = None, None
1036 rejects += 1
1036 rejects += 1
1037 continue
1037 continue
1038 elif state == 'git':
1038 elif state == 'git':
1039 gitpatches = values
1039 gitpatches = values
1040 cwd = os.getcwd()
1040 cwd = os.getcwd()
1041 for gp in gitpatches:
1041 for gp in gitpatches:
1042 if gp.op in ('COPY', 'RENAME'):
1042 if gp.op in ('COPY', 'RENAME'):
1043 copyfile(gp.oldpath, gp.path, cwd)
1043 copyfile(gp.oldpath, gp.path, cwd)
1044 changed[gp.path] = gp
1044 changed[gp.path] = gp
1045 else:
1045 else:
1046 raise util.Abort(_('unsupported parser state: %s') % state)
1046 raise util.Abort(_('unsupported parser state: %s') % state)
1047
1047
1048 rejects += closefile()
1048 rejects += closefile()
1049
1049
1050 if rejects:
1050 if rejects:
1051 return -1
1051 return -1
1052 return err
1052 return err
1053
1053
1054 def diffopts(ui, opts=None, untrusted=False):
1054 def diffopts(ui, opts=None, untrusted=False):
1055 def get(key, name=None, getter=ui.configbool):
1055 def get(key, name=None, getter=ui.configbool):
1056 return ((opts and opts.get(key)) or
1056 return ((opts and opts.get(key)) or
1057 getter('diff', name or key, None, untrusted=untrusted))
1057 getter('diff', name or key, None, untrusted=untrusted))
1058 return mdiff.diffopts(
1058 return mdiff.diffopts(
1059 text=opts and opts.get('text'),
1059 text=opts and opts.get('text'),
1060 git=get('git'),
1060 git=get('git'),
1061 nodates=get('nodates'),
1061 nodates=get('nodates'),
1062 showfunc=get('show_function', 'showfunc'),
1062 showfunc=get('show_function', 'showfunc'),
1063 ignorews=get('ignore_all_space', 'ignorews'),
1063 ignorews=get('ignore_all_space', 'ignorews'),
1064 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1064 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1065 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1065 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1066 context=get('unified', getter=ui.config))
1066 context=get('unified', getter=ui.config))
1067
1067
1068 def updatedir(ui, repo, patches, similarity=0):
1068 def updatedir(ui, repo, patches, similarity=0):
1069 '''Update dirstate after patch application according to metadata'''
1069 '''Update dirstate after patch application according to metadata'''
1070 if not patches:
1070 if not patches:
1071 return
1071 return
1072 copies = []
1072 copies = []
1073 removes = set()
1073 removes = set()
1074 cfiles = patches.keys()
1074 cfiles = patches.keys()
1075 cwd = repo.getcwd()
1075 cwd = repo.getcwd()
1076 if cwd:
1076 if cwd:
1077 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1077 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1078 for f in patches:
1078 for f in patches:
1079 gp = patches[f]
1079 gp = patches[f]
1080 if not gp:
1080 if not gp:
1081 continue
1081 continue
1082 if gp.op == 'RENAME':
1082 if gp.op == 'RENAME':
1083 copies.append((gp.oldpath, gp.path))
1083 copies.append((gp.oldpath, gp.path))
1084 removes.add(gp.oldpath)
1084 removes.add(gp.oldpath)
1085 elif gp.op == 'COPY':
1085 elif gp.op == 'COPY':
1086 copies.append((gp.oldpath, gp.path))
1086 copies.append((gp.oldpath, gp.path))
1087 elif gp.op == 'DELETE':
1087 elif gp.op == 'DELETE':
1088 removes.add(gp.path)
1088 removes.add(gp.path)
1089 for src, dst in copies:
1089 for src, dst in copies:
1090 repo.copy(src, dst)
1090 repo.copy(src, dst)
1091 if (not similarity) and removes:
1091 if (not similarity) and removes:
1092 repo.remove(sorted(removes), True)
1092 repo.remove(sorted(removes), True)
1093 for f in patches:
1093 for f in patches:
1094 gp = patches[f]
1094 gp = patches[f]
1095 if gp and gp.mode:
1095 if gp and gp.mode:
1096 islink, isexec = gp.mode
1096 islink, isexec = gp.mode
1097 dst = repo.wjoin(gp.path)
1097 dst = repo.wjoin(gp.path)
1098 # patch won't create empty files
1098 # patch won't create empty files
1099 if gp.op == 'ADD' and not os.path.exists(dst):
1099 if gp.op == 'ADD' and not os.path.exists(dst):
1100 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1100 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1101 repo.wwrite(gp.path, '', flags)
1101 repo.wwrite(gp.path, '', flags)
1102 elif gp.op != 'DELETE':
1102 elif gp.op != 'DELETE':
1103 util.set_flags(dst, islink, isexec)
1103 util.set_flags(dst, islink, isexec)
1104 cmdutil.addremove(repo, cfiles, similarity=similarity)
1104 cmdutil.addremove(repo, cfiles, similarity=similarity)
1105 files = patches.keys()
1105 files = patches.keys()
1106 files.extend([r for r in removes if r not in files])
1106 files.extend([r for r in removes if r not in files])
1107 return sorted(files)
1107 return sorted(files)
1108
1108
1109 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1109 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1110 """use <patcher> to apply <patchname> to the working directory.
1110 """use <patcher> to apply <patchname> to the working directory.
1111 returns whether patch was applied with fuzz factor."""
1111 returns whether patch was applied with fuzz factor."""
1112
1112
1113 fuzz = False
1113 fuzz = False
1114 if cwd:
1114 if cwd:
1115 args.append('-d %s' % util.shellquote(cwd))
1115 args.append('-d %s' % util.shellquote(cwd))
1116 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1116 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1117 util.shellquote(patchname)))
1117 util.shellquote(patchname)))
1118
1118
1119 for line in fp:
1119 for line in fp:
1120 line = line.rstrip()
1120 line = line.rstrip()
1121 ui.note(line + '\n')
1121 ui.note(line + '\n')
1122 if line.startswith('patching file '):
1122 if line.startswith('patching file '):
1123 pf = util.parse_patch_output(line)
1123 pf = util.parse_patch_output(line)
1124 printed_file = False
1124 printed_file = False
1125 files.setdefault(pf, None)
1125 files.setdefault(pf, None)
1126 elif line.find('with fuzz') >= 0:
1126 elif line.find('with fuzz') >= 0:
1127 fuzz = True
1127 fuzz = True
1128 if not printed_file:
1128 if not printed_file:
1129 ui.warn(pf + '\n')
1129 ui.warn(pf + '\n')
1130 printed_file = True
1130 printed_file = True
1131 ui.warn(line + '\n')
1131 ui.warn(line + '\n')
1132 elif line.find('saving rejects to file') >= 0:
1132 elif line.find('saving rejects to file') >= 0:
1133 ui.warn(line + '\n')
1133 ui.warn(line + '\n')
1134 elif line.find('FAILED') >= 0:
1134 elif line.find('FAILED') >= 0:
1135 if not printed_file:
1135 if not printed_file:
1136 ui.warn(pf + '\n')
1136 ui.warn(pf + '\n')
1137 printed_file = True
1137 printed_file = True
1138 ui.warn(line + '\n')
1138 ui.warn(line + '\n')
1139 code = fp.close()
1139 code = fp.close()
1140 if code:
1140 if code:
1141 raise PatchError(_("patch command failed: %s") %
1141 raise PatchError(_("patch command failed: %s") %
1142 util.explain_exit(code)[0])
1142 util.explain_exit(code)[0])
1143 return fuzz
1143 return fuzz
1144
1144
1145 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1145 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1146 """use builtin patch to apply <patchobj> to the working directory.
1146 """use builtin patch to apply <patchobj> to the working directory.
1147 returns whether patch was applied with fuzz factor."""
1147 returns whether patch was applied with fuzz factor."""
1148
1148
1149 if files is None:
1149 if files is None:
1150 files = {}
1150 files = {}
1151 if eolmode is None:
1151 if eolmode is None:
1152 eolmode = ui.config('patch', 'eol', 'strict')
1152 eolmode = ui.config('patch', 'eol', 'strict')
1153 if eolmode.lower() not in eolmodes:
1153 if eolmode.lower() not in eolmodes:
1154 raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
1154 raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
1155 eolmode = eolmode.lower()
1155 eolmode = eolmode.lower()
1156
1156
1157 try:
1157 try:
1158 fp = open(patchobj, 'rb')
1158 fp = open(patchobj, 'rb')
1159 except TypeError:
1159 except TypeError:
1160 fp = patchobj
1160 fp = patchobj
1161 if cwd:
1161 if cwd:
1162 curdir = os.getcwd()
1162 curdir = os.getcwd()
1163 os.chdir(cwd)
1163 os.chdir(cwd)
1164 try:
1164 try:
1165 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1165 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1166 finally:
1166 finally:
1167 if cwd:
1167 if cwd:
1168 os.chdir(curdir)
1168 os.chdir(curdir)
1169 if fp != patchobj:
1170 fp.close()
1169 if ret < 0:
1171 if ret < 0:
1170 raise PatchError
1172 raise PatchError
1171 return ret > 0
1173 return ret > 0
1172
1174
1173 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1175 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1174 """Apply <patchname> to the working directory.
1176 """Apply <patchname> to the working directory.
1175
1177
1176 'eolmode' specifies how end of lines should be handled. It can be:
1178 'eolmode' specifies how end of lines should be handled. It can be:
1177 - 'strict': inputs are read in binary mode, EOLs are preserved
1179 - 'strict': inputs are read in binary mode, EOLs are preserved
1178 - 'crlf': EOLs are ignored when patching and reset to CRLF
1180 - 'crlf': EOLs are ignored when patching and reset to CRLF
1179 - 'lf': EOLs are ignored when patching and reset to LF
1181 - 'lf': EOLs are ignored when patching and reset to LF
1180 - None: get it from user settings, default to 'strict'
1182 - None: get it from user settings, default to 'strict'
1181 'eolmode' is ignored when using an external patcher program.
1183 'eolmode' is ignored when using an external patcher program.
1182
1184
1183 Returns whether patch was applied with fuzz factor.
1185 Returns whether patch was applied with fuzz factor.
1184 """
1186 """
1185 patcher = ui.config('ui', 'patch')
1187 patcher = ui.config('ui', 'patch')
1186 args = []
1188 args = []
1187 if files is None:
1189 if files is None:
1188 files = {}
1190 files = {}
1189 try:
1191 try:
1190 if patcher:
1192 if patcher:
1191 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1193 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1192 files)
1194 files)
1193 else:
1195 else:
1194 try:
1196 try:
1195 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1197 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1196 except NoHunks:
1198 except NoHunks:
1197 patcher = util.find_exe('gpatch') or util.find_exe('patch') or 'patch'
1199 patcher = util.find_exe('gpatch') or util.find_exe('patch') or 'patch'
1198 ui.debug('no valid hunks found; trying with %r instead\n' %
1200 ui.debug('no valid hunks found; trying with %r instead\n' %
1199 patcher)
1201 patcher)
1200 if util.needbinarypatch():
1202 if util.needbinarypatch():
1201 args.append('--binary')
1203 args.append('--binary')
1202 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1204 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1203 files)
1205 files)
1204 except PatchError, err:
1206 except PatchError, err:
1205 s = str(err)
1207 s = str(err)
1206 if s:
1208 if s:
1207 raise util.Abort(s)
1209 raise util.Abort(s)
1208 else:
1210 else:
1209 raise util.Abort(_('patch failed to apply'))
1211 raise util.Abort(_('patch failed to apply'))
1210
1212
1211 def b85diff(to, tn):
1213 def b85diff(to, tn):
1212 '''print base85-encoded binary diff'''
1214 '''print base85-encoded binary diff'''
1213 def gitindex(text):
1215 def gitindex(text):
1214 if not text:
1216 if not text:
1215 return '0' * 40
1217 return '0' * 40
1216 l = len(text)
1218 l = len(text)
1217 s = util.sha1('blob %d\0' % l)
1219 s = util.sha1('blob %d\0' % l)
1218 s.update(text)
1220 s.update(text)
1219 return s.hexdigest()
1221 return s.hexdigest()
1220
1222
1221 def fmtline(line):
1223 def fmtline(line):
1222 l = len(line)
1224 l = len(line)
1223 if l <= 26:
1225 if l <= 26:
1224 l = chr(ord('A') + l - 1)
1226 l = chr(ord('A') + l - 1)
1225 else:
1227 else:
1226 l = chr(l - 26 + ord('a') - 1)
1228 l = chr(l - 26 + ord('a') - 1)
1227 return '%c%s\n' % (l, base85.b85encode(line, True))
1229 return '%c%s\n' % (l, base85.b85encode(line, True))
1228
1230
1229 def chunk(text, csize=52):
1231 def chunk(text, csize=52):
1230 l = len(text)
1232 l = len(text)
1231 i = 0
1233 i = 0
1232 while i < l:
1234 while i < l:
1233 yield text[i:i+csize]
1235 yield text[i:i+csize]
1234 i += csize
1236 i += csize
1235
1237
1236 tohash = gitindex(to)
1238 tohash = gitindex(to)
1237 tnhash = gitindex(tn)
1239 tnhash = gitindex(tn)
1238 if tohash == tnhash:
1240 if tohash == tnhash:
1239 return ""
1241 return ""
1240
1242
1241 # TODO: deltas
1243 # TODO: deltas
1242 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1244 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1243 (tohash, tnhash, len(tn))]
1245 (tohash, tnhash, len(tn))]
1244 for l in chunk(zlib.compress(tn)):
1246 for l in chunk(zlib.compress(tn)):
1245 ret.append(fmtline(l))
1247 ret.append(fmtline(l))
1246 ret.append('\n')
1248 ret.append('\n')
1247 return ''.join(ret)
1249 return ''.join(ret)
1248
1250
1249 class GitDiffRequired(Exception):
1251 class GitDiffRequired(Exception):
1250 pass
1252 pass
1251
1253
1252 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1254 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1253 losedatafn=None):
1255 losedatafn=None):
1254 '''yields diff of changes to files between two nodes, or node and
1256 '''yields diff of changes to files between two nodes, or node and
1255 working directory.
1257 working directory.
1256
1258
1257 if node1 is None, use first dirstate parent instead.
1259 if node1 is None, use first dirstate parent instead.
1258 if node2 is None, compare node1 with working directory.
1260 if node2 is None, compare node1 with working directory.
1259
1261
1260 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1262 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1261 every time some change cannot be represented with the current
1263 every time some change cannot be represented with the current
1262 patch format. Return False to upgrade to git patch format, True to
1264 patch format. Return False to upgrade to git patch format, True to
1263 accept the loss or raise an exception to abort the diff. It is
1265 accept the loss or raise an exception to abort the diff. It is
1264 called with the name of current file being diffed as 'fn'. If set
1266 called with the name of current file being diffed as 'fn'. If set
1265 to None, patches will always be upgraded to git format when
1267 to None, patches will always be upgraded to git format when
1266 necessary.
1268 necessary.
1267 '''
1269 '''
1268
1270
1269 if opts is None:
1271 if opts is None:
1270 opts = mdiff.defaultopts
1272 opts = mdiff.defaultopts
1271
1273
1272 if not node1 and not node2:
1274 if not node1 and not node2:
1273 node1 = repo.dirstate.parents()[0]
1275 node1 = repo.dirstate.parents()[0]
1274
1276
1275 def lrugetfilectx():
1277 def lrugetfilectx():
1276 cache = {}
1278 cache = {}
1277 order = []
1279 order = []
1278 def getfilectx(f, ctx):
1280 def getfilectx(f, ctx):
1279 fctx = ctx.filectx(f, filelog=cache.get(f))
1281 fctx = ctx.filectx(f, filelog=cache.get(f))
1280 if f not in cache:
1282 if f not in cache:
1281 if len(cache) > 20:
1283 if len(cache) > 20:
1282 del cache[order.pop(0)]
1284 del cache[order.pop(0)]
1283 cache[f] = fctx.filelog()
1285 cache[f] = fctx.filelog()
1284 else:
1286 else:
1285 order.remove(f)
1287 order.remove(f)
1286 order.append(f)
1288 order.append(f)
1287 return fctx
1289 return fctx
1288 return getfilectx
1290 return getfilectx
1289 getfilectx = lrugetfilectx()
1291 getfilectx = lrugetfilectx()
1290
1292
1291 ctx1 = repo[node1]
1293 ctx1 = repo[node1]
1292 ctx2 = repo[node2]
1294 ctx2 = repo[node2]
1293
1295
1294 if not changes:
1296 if not changes:
1295 changes = repo.status(ctx1, ctx2, match=match)
1297 changes = repo.status(ctx1, ctx2, match=match)
1296 modified, added, removed = changes[:3]
1298 modified, added, removed = changes[:3]
1297
1299
1298 if not modified and not added and not removed:
1300 if not modified and not added and not removed:
1299 return []
1301 return []
1300
1302
1301 revs = None
1303 revs = None
1302 if not repo.ui.quiet:
1304 if not repo.ui.quiet:
1303 hexfunc = repo.ui.debugflag and hex or short
1305 hexfunc = repo.ui.debugflag and hex or short
1304 revs = [hexfunc(node) for node in [node1, node2] if node]
1306 revs = [hexfunc(node) for node in [node1, node2] if node]
1305
1307
1306 copy = {}
1308 copy = {}
1307 if opts.git or opts.upgrade:
1309 if opts.git or opts.upgrade:
1308 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1310 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1309 copy = copy.copy()
1311 copy = copy.copy()
1310 for k, v in copy.items():
1312 for k, v in copy.items():
1311 copy[v] = k
1313 copy[v] = k
1312
1314
1313 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1315 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1314 modified, added, removed, copy, getfilectx, opts, losedata)
1316 modified, added, removed, copy, getfilectx, opts, losedata)
1315 if opts.upgrade and not opts.git:
1317 if opts.upgrade and not opts.git:
1316 try:
1318 try:
1317 def losedata(fn):
1319 def losedata(fn):
1318 if not losedatafn or not losedatafn(fn=fn):
1320 if not losedatafn or not losedatafn(fn=fn):
1319 raise GitDiffRequired()
1321 raise GitDiffRequired()
1320 # Buffer the whole output until we are sure it can be generated
1322 # Buffer the whole output until we are sure it can be generated
1321 return list(difffn(opts.copy(git=False), losedata))
1323 return list(difffn(opts.copy(git=False), losedata))
1322 except GitDiffRequired:
1324 except GitDiffRequired:
1323 return difffn(opts.copy(git=True), None)
1325 return difffn(opts.copy(git=True), None)
1324 else:
1326 else:
1325 return difffn(opts, None)
1327 return difffn(opts, None)
1326
1328
1327 def _addmodehdr(header, omode, nmode):
1329 def _addmodehdr(header, omode, nmode):
1328 if omode != nmode:
1330 if omode != nmode:
1329 header.append('old mode %s\n' % omode)
1331 header.append('old mode %s\n' % omode)
1330 header.append('new mode %s\n' % nmode)
1332 header.append('new mode %s\n' % nmode)
1331
1333
1332 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1334 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1333 copy, getfilectx, opts, losedatafn):
1335 copy, getfilectx, opts, losedatafn):
1334
1336
1335 date1 = util.datestr(ctx1.date())
1337 date1 = util.datestr(ctx1.date())
1336 man1 = ctx1.manifest()
1338 man1 = ctx1.manifest()
1337
1339
1338 gone = set()
1340 gone = set()
1339 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1341 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1340
1342
1341 if opts.git:
1343 if opts.git:
1342 revs = None
1344 revs = None
1343
1345
1344 for f in sorted(modified + added + removed):
1346 for f in sorted(modified + added + removed):
1345 to = None
1347 to = None
1346 tn = None
1348 tn = None
1347 dodiff = True
1349 dodiff = True
1348 header = []
1350 header = []
1349 if f in man1:
1351 if f in man1:
1350 to = getfilectx(f, ctx1).data()
1352 to = getfilectx(f, ctx1).data()
1351 if f not in removed:
1353 if f not in removed:
1352 tn = getfilectx(f, ctx2).data()
1354 tn = getfilectx(f, ctx2).data()
1353 a, b = f, f
1355 a, b = f, f
1354 if opts.git or losedatafn:
1356 if opts.git or losedatafn:
1355 if f in added:
1357 if f in added:
1356 mode = gitmode[ctx2.flags(f)]
1358 mode = gitmode[ctx2.flags(f)]
1357 if f in copy:
1359 if f in copy:
1358 if opts.git:
1360 if opts.git:
1359 a = copy[f]
1361 a = copy[f]
1360 omode = gitmode[man1.flags(a)]
1362 omode = gitmode[man1.flags(a)]
1361 _addmodehdr(header, omode, mode)
1363 _addmodehdr(header, omode, mode)
1362 if a in removed and a not in gone:
1364 if a in removed and a not in gone:
1363 op = 'rename'
1365 op = 'rename'
1364 gone.add(a)
1366 gone.add(a)
1365 else:
1367 else:
1366 op = 'copy'
1368 op = 'copy'
1367 header.append('%s from %s\n' % (op, a))
1369 header.append('%s from %s\n' % (op, a))
1368 header.append('%s to %s\n' % (op, f))
1370 header.append('%s to %s\n' % (op, f))
1369 to = getfilectx(a, ctx1).data()
1371 to = getfilectx(a, ctx1).data()
1370 else:
1372 else:
1371 losedatafn(f)
1373 losedatafn(f)
1372 else:
1374 else:
1373 if opts.git:
1375 if opts.git:
1374 header.append('new file mode %s\n' % mode)
1376 header.append('new file mode %s\n' % mode)
1375 elif ctx2.flags(f):
1377 elif ctx2.flags(f):
1376 losedatafn(f)
1378 losedatafn(f)
1377 if util.binary(tn):
1379 if util.binary(tn):
1378 if opts.git:
1380 if opts.git:
1379 dodiff = 'binary'
1381 dodiff = 'binary'
1380 else:
1382 else:
1381 losedatafn(f)
1383 losedatafn(f)
1382 if not opts.git and not tn:
1384 if not opts.git and not tn:
1383 # regular diffs cannot represent new empty file
1385 # regular diffs cannot represent new empty file
1384 losedatafn(f)
1386 losedatafn(f)
1385 elif f in removed:
1387 elif f in removed:
1386 if opts.git:
1388 if opts.git:
1387 # have we already reported a copy above?
1389 # have we already reported a copy above?
1388 if f in copy and copy[f] in added and copy[copy[f]] == f:
1390 if f in copy and copy[f] in added and copy[copy[f]] == f:
1389 dodiff = False
1391 dodiff = False
1390 else:
1392 else:
1391 header.append('deleted file mode %s\n' %
1393 header.append('deleted file mode %s\n' %
1392 gitmode[man1.flags(f)])
1394 gitmode[man1.flags(f)])
1393 elif not to:
1395 elif not to:
1394 # regular diffs cannot represent empty file deletion
1396 # regular diffs cannot represent empty file deletion
1395 losedatafn(f)
1397 losedatafn(f)
1396 else:
1398 else:
1397 oflag = man1.flags(f)
1399 oflag = man1.flags(f)
1398 nflag = ctx2.flags(f)
1400 nflag = ctx2.flags(f)
1399 binary = util.binary(to) or util.binary(tn)
1401 binary = util.binary(to) or util.binary(tn)
1400 if opts.git:
1402 if opts.git:
1401 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1403 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1402 if binary:
1404 if binary:
1403 dodiff = 'binary'
1405 dodiff = 'binary'
1404 elif binary or nflag != oflag:
1406 elif binary or nflag != oflag:
1405 losedatafn(f)
1407 losedatafn(f)
1406 if opts.git:
1408 if opts.git:
1407 header.insert(0, mdiff.diffline(revs, a, b, opts))
1409 header.insert(0, mdiff.diffline(revs, a, b, opts))
1408
1410
1409 if dodiff:
1411 if dodiff:
1410 if dodiff == 'binary':
1412 if dodiff == 'binary':
1411 text = b85diff(to, tn)
1413 text = b85diff(to, tn)
1412 else:
1414 else:
1413 text = mdiff.unidiff(to, date1,
1415 text = mdiff.unidiff(to, date1,
1414 # ctx2 date may be dynamic
1416 # ctx2 date may be dynamic
1415 tn, util.datestr(ctx2.date()),
1417 tn, util.datestr(ctx2.date()),
1416 a, b, revs, opts=opts)
1418 a, b, revs, opts=opts)
1417 if header and (text or len(header) > 1):
1419 if header and (text or len(header) > 1):
1418 yield ''.join(header)
1420 yield ''.join(header)
1419 if text:
1421 if text:
1420 yield text
1422 yield text
1421
1423
1422 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1424 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1423 opts=None):
1425 opts=None):
1424 '''export changesets as hg patches.'''
1426 '''export changesets as hg patches.'''
1425
1427
1426 total = len(revs)
1428 total = len(revs)
1427 revwidth = max([len(str(rev)) for rev in revs])
1429 revwidth = max([len(str(rev)) for rev in revs])
1428
1430
1429 def single(rev, seqno, fp):
1431 def single(rev, seqno, fp):
1430 ctx = repo[rev]
1432 ctx = repo[rev]
1431 node = ctx.node()
1433 node = ctx.node()
1432 parents = [p.node() for p in ctx.parents() if p]
1434 parents = [p.node() for p in ctx.parents() if p]
1433 branch = ctx.branch()
1435 branch = ctx.branch()
1434 if switch_parent:
1436 if switch_parent:
1435 parents.reverse()
1437 parents.reverse()
1436 prev = (parents and parents[0]) or nullid
1438 prev = (parents and parents[0]) or nullid
1437
1439
1438 if not fp:
1440 if not fp:
1439 fp = cmdutil.make_file(repo, template, node, total=total,
1441 fp = cmdutil.make_file(repo, template, node, total=total,
1440 seqno=seqno, revwidth=revwidth,
1442 seqno=seqno, revwidth=revwidth,
1441 mode='ab')
1443 mode='ab')
1442 if fp != sys.stdout and hasattr(fp, 'name'):
1444 if fp != sys.stdout and hasattr(fp, 'name'):
1443 repo.ui.note("%s\n" % fp.name)
1445 repo.ui.note("%s\n" % fp.name)
1444
1446
1445 fp.write("# HG changeset patch\n")
1447 fp.write("# HG changeset patch\n")
1446 fp.write("# User %s\n" % ctx.user())
1448 fp.write("# User %s\n" % ctx.user())
1447 fp.write("# Date %d %d\n" % ctx.date())
1449 fp.write("# Date %d %d\n" % ctx.date())
1448 if branch and (branch != 'default'):
1450 if branch and (branch != 'default'):
1449 fp.write("# Branch %s\n" % branch)
1451 fp.write("# Branch %s\n" % branch)
1450 fp.write("# Node ID %s\n" % hex(node))
1452 fp.write("# Node ID %s\n" % hex(node))
1451 fp.write("# Parent %s\n" % hex(prev))
1453 fp.write("# Parent %s\n" % hex(prev))
1452 if len(parents) > 1:
1454 if len(parents) > 1:
1453 fp.write("# Parent %s\n" % hex(parents[1]))
1455 fp.write("# Parent %s\n" % hex(parents[1]))
1454 fp.write(ctx.description().rstrip())
1456 fp.write(ctx.description().rstrip())
1455 fp.write("\n\n")
1457 fp.write("\n\n")
1456
1458
1457 for chunk in diff(repo, prev, node, opts=opts):
1459 for chunk in diff(repo, prev, node, opts=opts):
1458 fp.write(chunk)
1460 fp.write(chunk)
1459
1461
1460 for seqno, rev in enumerate(revs):
1462 for seqno, rev in enumerate(revs):
1461 single(rev, seqno+1, fp)
1463 single(rev, seqno+1, fp)
1462
1464
1463 def diffstatdata(lines):
1465 def diffstatdata(lines):
1464 filename, adds, removes = None, 0, 0
1466 filename, adds, removes = None, 0, 0
1465 for line in lines:
1467 for line in lines:
1466 if line.startswith('diff'):
1468 if line.startswith('diff'):
1467 if filename:
1469 if filename:
1468 isbinary = adds == 0 and removes == 0
1470 isbinary = adds == 0 and removes == 0
1469 yield (filename, adds, removes, isbinary)
1471 yield (filename, adds, removes, isbinary)
1470 # set numbers to 0 anyway when starting new file
1472 # set numbers to 0 anyway when starting new file
1471 adds, removes = 0, 0
1473 adds, removes = 0, 0
1472 if line.startswith('diff --git'):
1474 if line.startswith('diff --git'):
1473 filename = gitre.search(line).group(1)
1475 filename = gitre.search(line).group(1)
1474 else:
1476 else:
1475 # format: "diff -r ... -r ... filename"
1477 # format: "diff -r ... -r ... filename"
1476 filename = line.split(None, 5)[-1]
1478 filename = line.split(None, 5)[-1]
1477 elif line.startswith('+') and not line.startswith('+++'):
1479 elif line.startswith('+') and not line.startswith('+++'):
1478 adds += 1
1480 adds += 1
1479 elif line.startswith('-') and not line.startswith('---'):
1481 elif line.startswith('-') and not line.startswith('---'):
1480 removes += 1
1482 removes += 1
1481 if filename:
1483 if filename:
1482 isbinary = adds == 0 and removes == 0
1484 isbinary = adds == 0 and removes == 0
1483 yield (filename, adds, removes, isbinary)
1485 yield (filename, adds, removes, isbinary)
1484
1486
1485 def diffstat(lines, width=80, git=False):
1487 def diffstat(lines, width=80, git=False):
1486 output = []
1488 output = []
1487 stats = list(diffstatdata(lines))
1489 stats = list(diffstatdata(lines))
1488
1490
1489 maxtotal, maxname = 0, 0
1491 maxtotal, maxname = 0, 0
1490 totaladds, totalremoves = 0, 0
1492 totaladds, totalremoves = 0, 0
1491 hasbinary = False
1493 hasbinary = False
1492 for filename, adds, removes, isbinary in stats:
1494 for filename, adds, removes, isbinary in stats:
1493 totaladds += adds
1495 totaladds += adds
1494 totalremoves += removes
1496 totalremoves += removes
1495 maxname = max(maxname, len(filename))
1497 maxname = max(maxname, len(filename))
1496 maxtotal = max(maxtotal, adds+removes)
1498 maxtotal = max(maxtotal, adds+removes)
1497 if isbinary:
1499 if isbinary:
1498 hasbinary = True
1500 hasbinary = True
1499
1501
1500 countwidth = len(str(maxtotal))
1502 countwidth = len(str(maxtotal))
1501 if hasbinary and countwidth < 3:
1503 if hasbinary and countwidth < 3:
1502 countwidth = 3
1504 countwidth = 3
1503 graphwidth = width - countwidth - maxname - 6
1505 graphwidth = width - countwidth - maxname - 6
1504 if graphwidth < 10:
1506 if graphwidth < 10:
1505 graphwidth = 10
1507 graphwidth = 10
1506
1508
1507 def scale(i):
1509 def scale(i):
1508 if maxtotal <= graphwidth:
1510 if maxtotal <= graphwidth:
1509 return i
1511 return i
1510 # If diffstat runs out of room it doesn't print anything,
1512 # If diffstat runs out of room it doesn't print anything,
1511 # which isn't very useful, so always print at least one + or -
1513 # which isn't very useful, so always print at least one + or -
1512 # if there were at least some changes.
1514 # if there were at least some changes.
1513 return max(i * graphwidth // maxtotal, int(bool(i)))
1515 return max(i * graphwidth // maxtotal, int(bool(i)))
1514
1516
1515 for filename, adds, removes, isbinary in stats:
1517 for filename, adds, removes, isbinary in stats:
1516 if git and isbinary:
1518 if git and isbinary:
1517 count = 'Bin'
1519 count = 'Bin'
1518 else:
1520 else:
1519 count = adds + removes
1521 count = adds + removes
1520 pluses = '+' * scale(adds)
1522 pluses = '+' * scale(adds)
1521 minuses = '-' * scale(removes)
1523 minuses = '-' * scale(removes)
1522 output.append(' %-*s | %*s %s%s\n' % (maxname, filename, countwidth,
1524 output.append(' %-*s | %*s %s%s\n' % (maxname, filename, countwidth,
1523 count, pluses, minuses))
1525 count, pluses, minuses))
1524
1526
1525 if stats:
1527 if stats:
1526 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1528 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1527 % (len(stats), totaladds, totalremoves))
1529 % (len(stats), totaladds, totalremoves))
1528
1530
1529 return ''.join(output)
1531 return ''.join(output)
General Comments 0
You need to be logged in to leave comments. Login now