##// END OF EJS Templates
patch.py: don't mark files as changed unless they have actually been changed
Bryan O'Sullivan -
r4899:1b7bbc43 default
parent child Browse files
Show More
@@ -1,1255 +1,1258 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
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 from i18n import _
9 from i18n import _
10 from node import *
10 from node import *
11 import base85, cmdutil, mdiff, util, context, revlog, diffhelpers
11 import base85, cmdutil, mdiff, util, context, revlog, diffhelpers
12 import cStringIO, email.Parser, os, popen2, re, sha
12 import cStringIO, email.Parser, os, popen2, re, sha
13 import sys, tempfile, zlib
13 import sys, tempfile, zlib
14
14
15 class PatchError(Exception):
15 class PatchError(Exception):
16 pass
16 pass
17
17
18 # helper functions
18 # helper functions
19
19
20 def copyfile(src, dst, basedir=None):
20 def copyfile(src, dst, basedir=None):
21 if not basedir:
21 if not basedir:
22 basedir = os.getcwd()
22 basedir = os.getcwd()
23
23
24 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
24 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
25 if os.path.exists(absdst):
25 if os.path.exists(absdst):
26 raise util.Abort(_("cannot create %s: destination already exists") %
26 raise util.Abort(_("cannot create %s: destination already exists") %
27 dst)
27 dst)
28
28
29 targetdir = os.path.dirname(absdst)
29 targetdir = os.path.dirname(absdst)
30 if not os.path.isdir(targetdir):
30 if not os.path.isdir(targetdir):
31 os.makedirs(targetdir)
31 os.makedirs(targetdir)
32
32
33 util.copyfile(abssrc, absdst)
33 util.copyfile(abssrc, absdst)
34
34
35 # public functions
35 # public functions
36
36
37 def extract(ui, fileobj):
37 def extract(ui, fileobj):
38 '''extract patch from data read from fileobj.
38 '''extract patch from data read from fileobj.
39
39
40 patch can be a normal patch or contained in an email message.
40 patch can be a normal patch or contained in an email message.
41
41
42 return tuple (filename, message, user, date, node, p1, p2).
42 return tuple (filename, message, user, date, node, p1, p2).
43 Any item in the returned tuple can be None. If filename is None,
43 Any item in the returned tuple can be None. If filename is None,
44 fileobj did not contain a patch. Caller must unlink filename when done.'''
44 fileobj did not contain a patch. Caller must unlink filename when done.'''
45
45
46 # attempt to detect the start of a patch
46 # attempt to detect the start of a patch
47 # (this heuristic is borrowed from quilt)
47 # (this heuristic is borrowed from quilt)
48 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
48 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
49 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
49 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
50 '(---|\*\*\*)[ \t])', re.MULTILINE)
50 '(---|\*\*\*)[ \t])', re.MULTILINE)
51
51
52 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
52 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
53 tmpfp = os.fdopen(fd, 'w')
53 tmpfp = os.fdopen(fd, 'w')
54 try:
54 try:
55 msg = email.Parser.Parser().parse(fileobj)
55 msg = email.Parser.Parser().parse(fileobj)
56
56
57 subject = msg['Subject']
57 subject = msg['Subject']
58 user = msg['From']
58 user = msg['From']
59 # should try to parse msg['Date']
59 # should try to parse msg['Date']
60 date = None
60 date = None
61 nodeid = None
61 nodeid = None
62 branch = None
62 branch = None
63 parents = []
63 parents = []
64
64
65 if subject:
65 if subject:
66 if subject.startswith('[PATCH'):
66 if subject.startswith('[PATCH'):
67 pend = subject.find(']')
67 pend = subject.find(']')
68 if pend >= 0:
68 if pend >= 0:
69 subject = subject[pend+1:].lstrip()
69 subject = subject[pend+1:].lstrip()
70 subject = subject.replace('\n\t', ' ')
70 subject = subject.replace('\n\t', ' ')
71 ui.debug('Subject: %s\n' % subject)
71 ui.debug('Subject: %s\n' % subject)
72 if user:
72 if user:
73 ui.debug('From: %s\n' % user)
73 ui.debug('From: %s\n' % user)
74 diffs_seen = 0
74 diffs_seen = 0
75 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
75 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
76
76
77 for part in msg.walk():
77 for part in msg.walk():
78 content_type = part.get_content_type()
78 content_type = part.get_content_type()
79 ui.debug('Content-Type: %s\n' % content_type)
79 ui.debug('Content-Type: %s\n' % content_type)
80 if content_type not in ok_types:
80 if content_type not in ok_types:
81 continue
81 continue
82 payload = part.get_payload(decode=True)
82 payload = part.get_payload(decode=True)
83 m = diffre.search(payload)
83 m = diffre.search(payload)
84 if m:
84 if m:
85 hgpatch = False
85 hgpatch = False
86 ignoretext = False
86 ignoretext = False
87
87
88 ui.debug(_('found patch at byte %d\n') % m.start(0))
88 ui.debug(_('found patch at byte %d\n') % m.start(0))
89 diffs_seen += 1
89 diffs_seen += 1
90 cfp = cStringIO.StringIO()
90 cfp = cStringIO.StringIO()
91 for line in payload[:m.start(0)].splitlines():
91 for line in payload[:m.start(0)].splitlines():
92 if line.startswith('# HG changeset patch'):
92 if line.startswith('# HG changeset patch'):
93 ui.debug(_('patch generated by hg export\n'))
93 ui.debug(_('patch generated by hg export\n'))
94 hgpatch = True
94 hgpatch = True
95 # drop earlier commit message content
95 # drop earlier commit message content
96 cfp.seek(0)
96 cfp.seek(0)
97 cfp.truncate()
97 cfp.truncate()
98 subject = None
98 subject = None
99 elif hgpatch:
99 elif hgpatch:
100 if line.startswith('# User '):
100 if line.startswith('# User '):
101 user = line[7:]
101 user = line[7:]
102 ui.debug('From: %s\n' % user)
102 ui.debug('From: %s\n' % user)
103 elif line.startswith("# Date "):
103 elif line.startswith("# Date "):
104 date = line[7:]
104 date = line[7:]
105 elif line.startswith("# Branch "):
105 elif line.startswith("# Branch "):
106 branch = line[9:]
106 branch = line[9:]
107 elif line.startswith("# Node ID "):
107 elif line.startswith("# Node ID "):
108 nodeid = line[10:]
108 nodeid = line[10:]
109 elif line.startswith("# Parent "):
109 elif line.startswith("# Parent "):
110 parents.append(line[10:])
110 parents.append(line[10:])
111 elif line == '---' and 'git-send-email' in msg['X-Mailer']:
111 elif line == '---' and 'git-send-email' in msg['X-Mailer']:
112 ignoretext = True
112 ignoretext = True
113 if not line.startswith('# ') and not ignoretext:
113 if not line.startswith('# ') and not ignoretext:
114 cfp.write(line)
114 cfp.write(line)
115 cfp.write('\n')
115 cfp.write('\n')
116 message = cfp.getvalue()
116 message = cfp.getvalue()
117 if tmpfp:
117 if tmpfp:
118 tmpfp.write(payload)
118 tmpfp.write(payload)
119 if not payload.endswith('\n'):
119 if not payload.endswith('\n'):
120 tmpfp.write('\n')
120 tmpfp.write('\n')
121 elif not diffs_seen and message and content_type == 'text/plain':
121 elif not diffs_seen and message and content_type == 'text/plain':
122 message += '\n' + payload
122 message += '\n' + payload
123 except:
123 except:
124 tmpfp.close()
124 tmpfp.close()
125 os.unlink(tmpname)
125 os.unlink(tmpname)
126 raise
126 raise
127
127
128 if subject and not message.startswith(subject):
128 if subject and not message.startswith(subject):
129 message = '%s\n%s' % (subject, message)
129 message = '%s\n%s' % (subject, message)
130 tmpfp.close()
130 tmpfp.close()
131 if not diffs_seen:
131 if not diffs_seen:
132 os.unlink(tmpname)
132 os.unlink(tmpname)
133 return None, message, user, date, branch, None, None, None
133 return None, message, user, date, branch, None, None, None
134 p1 = parents and parents.pop(0) or None
134 p1 = parents and parents.pop(0) or None
135 p2 = parents and parents.pop(0) or None
135 p2 = parents and parents.pop(0) or None
136 return tmpname, message, user, date, branch, nodeid, p1, p2
136 return tmpname, message, user, date, branch, nodeid, p1, p2
137
137
138 GP_PATCH = 1 << 0 # we have to run patch
138 GP_PATCH = 1 << 0 # we have to run patch
139 GP_FILTER = 1 << 1 # there's some copy/rename operation
139 GP_FILTER = 1 << 1 # there's some copy/rename operation
140 GP_BINARY = 1 << 2 # there's a binary patch
140 GP_BINARY = 1 << 2 # there's a binary patch
141
141
142 def readgitpatch(fp, firstline):
142 def readgitpatch(fp, firstline):
143 """extract git-style metadata about patches from <patchname>"""
143 """extract git-style metadata about patches from <patchname>"""
144 class gitpatch:
144 class gitpatch:
145 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
145 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
146 def __init__(self, path):
146 def __init__(self, path):
147 self.path = path
147 self.path = path
148 self.oldpath = None
148 self.oldpath = None
149 self.mode = None
149 self.mode = None
150 self.op = 'MODIFY'
150 self.op = 'MODIFY'
151 self.copymod = False
151 self.copymod = False
152 self.lineno = 0
152 self.lineno = 0
153 self.binary = False
153 self.binary = False
154
154
155 def reader(fp, firstline):
155 def reader(fp, firstline):
156 yield firstline
156 yield firstline
157 for line in fp:
157 for line in fp:
158 yield line
158 yield line
159
159
160 # Filter patch for git information
160 # Filter patch for git information
161 gitre = re.compile('diff --git a/(.*) b/(.*)')
161 gitre = re.compile('diff --git a/(.*) b/(.*)')
162 gp = None
162 gp = None
163 gitpatches = []
163 gitpatches = []
164 # Can have a git patch with only metadata, causing patch to complain
164 # Can have a git patch with only metadata, causing patch to complain
165 dopatch = 0
165 dopatch = 0
166
166
167 lineno = 0
167 lineno = 0
168 for line in reader(fp, firstline):
168 for line in reader(fp, firstline):
169 lineno += 1
169 lineno += 1
170 if line.startswith('diff --git'):
170 if line.startswith('diff --git'):
171 m = gitre.match(line)
171 m = gitre.match(line)
172 if m:
172 if m:
173 if gp:
173 if gp:
174 gitpatches.append(gp)
174 gitpatches.append(gp)
175 src, dst = m.group(1, 2)
175 src, dst = m.group(1, 2)
176 gp = gitpatch(dst)
176 gp = gitpatch(dst)
177 gp.lineno = lineno
177 gp.lineno = lineno
178 elif gp:
178 elif gp:
179 if line.startswith('--- '):
179 if line.startswith('--- '):
180 if gp.op in ('COPY', 'RENAME'):
180 if gp.op in ('COPY', 'RENAME'):
181 gp.copymod = True
181 gp.copymod = True
182 dopatch |= GP_FILTER
182 dopatch |= GP_FILTER
183 gitpatches.append(gp)
183 gitpatches.append(gp)
184 gp = None
184 gp = None
185 dopatch |= GP_PATCH
185 dopatch |= GP_PATCH
186 continue
186 continue
187 if line.startswith('rename from '):
187 if line.startswith('rename from '):
188 gp.op = 'RENAME'
188 gp.op = 'RENAME'
189 gp.oldpath = line[12:].rstrip()
189 gp.oldpath = line[12:].rstrip()
190 elif line.startswith('rename to '):
190 elif line.startswith('rename to '):
191 gp.path = line[10:].rstrip()
191 gp.path = line[10:].rstrip()
192 elif line.startswith('copy from '):
192 elif line.startswith('copy from '):
193 gp.op = 'COPY'
193 gp.op = 'COPY'
194 gp.oldpath = line[10:].rstrip()
194 gp.oldpath = line[10:].rstrip()
195 elif line.startswith('copy to '):
195 elif line.startswith('copy to '):
196 gp.path = line[8:].rstrip()
196 gp.path = line[8:].rstrip()
197 elif line.startswith('deleted file'):
197 elif line.startswith('deleted file'):
198 gp.op = 'DELETE'
198 gp.op = 'DELETE'
199 elif line.startswith('new file mode '):
199 elif line.startswith('new file mode '):
200 gp.op = 'ADD'
200 gp.op = 'ADD'
201 gp.mode = int(line.rstrip()[-3:], 8)
201 gp.mode = int(line.rstrip()[-3:], 8)
202 elif line.startswith('new mode '):
202 elif line.startswith('new mode '):
203 gp.mode = int(line.rstrip()[-3:], 8)
203 gp.mode = int(line.rstrip()[-3:], 8)
204 elif line.startswith('GIT binary patch'):
204 elif line.startswith('GIT binary patch'):
205 dopatch |= GP_BINARY
205 dopatch |= GP_BINARY
206 gp.binary = True
206 gp.binary = True
207 if gp:
207 if gp:
208 gitpatches.append(gp)
208 gitpatches.append(gp)
209
209
210 if not gitpatches:
210 if not gitpatches:
211 dopatch = GP_PATCH
211 dopatch = GP_PATCH
212
212
213 return (dopatch, gitpatches)
213 return (dopatch, gitpatches)
214
214
215 def patch(patchname, ui, strip=1, cwd=None, files={}):
215 def patch(patchname, ui, strip=1, cwd=None, files={}):
216 """apply the patch <patchname> to the working directory.
216 """apply the patch <patchname> to the working directory.
217 a list of patched files is returned"""
217 a list of patched files is returned"""
218 fp = file(patchname)
218 fp = file(patchname)
219 fuzz = False
219 fuzz = False
220 if cwd:
220 if cwd:
221 curdir = os.getcwd()
221 curdir = os.getcwd()
222 os.chdir(cwd)
222 os.chdir(cwd)
223 try:
223 try:
224 ret = applydiff(ui, fp, files, strip=strip)
224 ret = applydiff(ui, fp, files, strip=strip)
225 except PatchError, err:
225 except PatchError, err:
226 ui.debug(err)
226 ui.debug(err)
227 raise util.Abort(_("patch failed to apply"))
227 raise util.Abort(_("patch failed to apply"))
228 if cwd:
228 if cwd:
229 os.chdir(curdir)
229 os.chdir(curdir)
230 if ret < 0:
230 if ret < 0:
231 raise util.Abort(_("patch failed to apply"))
231 raise util.Abort(_("patch failed to apply"))
232 if ret > 0:
232 if ret > 0:
233 fuzz = True
233 fuzz = True
234 return fuzz
234 return fuzz
235
235
236 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
236 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
237 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
237 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
238 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
238 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
239
239
240 class patchfile:
240 class patchfile:
241 def __init__(self, ui, fname):
241 def __init__(self, ui, fname):
242 self.fname = fname
242 self.fname = fname
243 self.ui = ui
243 self.ui = ui
244 try:
244 try:
245 fp = file(fname, 'r')
245 fp = file(fname, 'r')
246 self.lines = fp.readlines()
246 self.lines = fp.readlines()
247 self.exists = True
247 self.exists = True
248 except IOError:
248 except IOError:
249 dirname = os.path.dirname(fname)
249 dirname = os.path.dirname(fname)
250 if dirname and not os.path.isdir(dirname):
250 if dirname and not os.path.isdir(dirname):
251 dirs = dirname.split(os.path.sep)
251 dirs = dirname.split(os.path.sep)
252 d = ""
252 d = ""
253 for x in dirs:
253 for x in dirs:
254 d = os.path.join(d, x)
254 d = os.path.join(d, x)
255 if not os.path.isdir(d):
255 if not os.path.isdir(d):
256 os.mkdir(d)
256 os.mkdir(d)
257 self.lines = []
257 self.lines = []
258 self.exists = False
258 self.exists = False
259
259
260 self.hash = {}
260 self.hash = {}
261 self.dirty = 0
261 self.dirty = 0
262 self.offset = 0
262 self.offset = 0
263 self.rej = []
263 self.rej = []
264 self.fileprinted = False
264 self.fileprinted = False
265 self.printfile(False)
265 self.printfile(False)
266 self.hunks = 0
266 self.hunks = 0
267
267
268 def printfile(self, warn):
268 def printfile(self, warn):
269 if self.fileprinted:
269 if self.fileprinted:
270 return
270 return
271 if warn or self.ui.verbose:
271 if warn or self.ui.verbose:
272 self.fileprinted = True
272 self.fileprinted = True
273 s = _("patching file %s\n") % self.fname
273 s = _("patching file %s\n") % self.fname
274 if warn:
274 if warn:
275 self.ui.warn(s)
275 self.ui.warn(s)
276 else:
276 else:
277 self.ui.note(s)
277 self.ui.note(s)
278
278
279
279
280 def findlines(self, l, linenum):
280 def findlines(self, l, linenum):
281 # looks through the hash and finds candidate lines. The
281 # looks through the hash and finds candidate lines. The
282 # result is a list of line numbers sorted based on distance
282 # result is a list of line numbers sorted based on distance
283 # from linenum
283 # from linenum
284 def sorter(a, b):
284 def sorter(a, b):
285 vala = abs(a - linenum)
285 vala = abs(a - linenum)
286 valb = abs(b - linenum)
286 valb = abs(b - linenum)
287 return cmp(vala, valb)
287 return cmp(vala, valb)
288
288
289 try:
289 try:
290 cand = self.hash[l]
290 cand = self.hash[l]
291 except:
291 except:
292 return []
292 return []
293
293
294 if len(cand) > 1:
294 if len(cand) > 1:
295 # resort our list of potentials forward then back.
295 # resort our list of potentials forward then back.
296 cand.sort(cmp=sorter)
296 cand.sort(cmp=sorter)
297 return cand
297 return cand
298
298
299 def hashlines(self):
299 def hashlines(self):
300 self.hash = {}
300 self.hash = {}
301 for x in xrange(len(self.lines)):
301 for x in xrange(len(self.lines)):
302 s = self.lines[x]
302 s = self.lines[x]
303 self.hash.setdefault(s, []).append(x)
303 self.hash.setdefault(s, []).append(x)
304
304
305 def write_rej(self):
305 def write_rej(self):
306 # our rejects are a little different from patch(1). This always
306 # our rejects are a little different from patch(1). This always
307 # creates rejects in the same form as the original patch. A file
307 # creates rejects in the same form as the original patch. A file
308 # header is inserted so that you can run the reject through patch again
308 # header is inserted so that you can run the reject through patch again
309 # without having to type the filename.
309 # without having to type the filename.
310
310
311 if not self.rej:
311 if not self.rej:
312 return
312 return
313 if self.hunks != 1:
313 if self.hunks != 1:
314 hunkstr = "s"
314 hunkstr = "s"
315 else:
315 else:
316 hunkstr = ""
316 hunkstr = ""
317
317
318 fname = self.fname + ".rej"
318 fname = self.fname + ".rej"
319 self.ui.warn(
319 self.ui.warn(
320 _("%d out of %d hunk%s FAILED -- saving rejects to file %s\n") %
320 _("%d out of %d hunk%s FAILED -- saving rejects to file %s\n") %
321 (len(self.rej), self.hunks, hunkstr, fname))
321 (len(self.rej), self.hunks, hunkstr, fname))
322 try: os.unlink(fname)
322 try: os.unlink(fname)
323 except:
323 except:
324 pass
324 pass
325 fp = file(fname, 'w')
325 fp = file(fname, 'w')
326 base = os.path.basename(self.fname)
326 base = os.path.basename(self.fname)
327 fp.write("--- %s\n+++ %s\n" % (base, base))
327 fp.write("--- %s\n+++ %s\n" % (base, base))
328 for x in self.rej:
328 for x in self.rej:
329 for l in x.hunk:
329 for l in x.hunk:
330 fp.write(l)
330 fp.write(l)
331 if l[-1] != '\n':
331 if l[-1] != '\n':
332 fp.write("\n\ No newline at end of file\n")
332 fp.write("\n\ No newline at end of file\n")
333
333
334 def write(self, dest=None):
334 def write(self, dest=None):
335 if self.dirty:
335 if self.dirty:
336 if not dest:
336 if not dest:
337 dest = self.fname
337 dest = self.fname
338 st = None
338 st = None
339 try:
339 try:
340 st = os.lstat(dest)
340 st = os.lstat(dest)
341 if st.st_nlink > 1:
341 if st.st_nlink > 1:
342 os.unlink(dest)
342 os.unlink(dest)
343 except: pass
343 except: pass
344 fp = file(dest, 'w')
344 fp = file(dest, 'w')
345 if st:
345 if st:
346 os.chmod(dest, st.st_mode)
346 os.chmod(dest, st.st_mode)
347 fp.writelines(self.lines)
347 fp.writelines(self.lines)
348 fp.close()
348 fp.close()
349
349
350 def close(self):
350 def close(self):
351 self.write()
351 self.write()
352 self.write_rej()
352 self.write_rej()
353
353
354 def apply(self, h, reverse):
354 def apply(self, h, reverse):
355 if not h.complete():
355 if not h.complete():
356 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
356 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
357 (h.number, h.desc, len(h.a), h.lena, len(h.b),
357 (h.number, h.desc, len(h.a), h.lena, len(h.b),
358 h.lenb))
358 h.lenb))
359
359
360 self.hunks += 1
360 self.hunks += 1
361 if reverse:
361 if reverse:
362 h.reverse()
362 h.reverse()
363
363
364 if self.exists and h.createfile():
364 if self.exists and h.createfile():
365 self.ui.warn(_("file %s already exists\n") % self.fname)
365 self.ui.warn(_("file %s already exists\n") % self.fname)
366 self.rej.append(h)
366 self.rej.append(h)
367 return -1
367 return -1
368
368
369 if isinstance(h, binhunk):
369 if isinstance(h, binhunk):
370 if h.rmfile():
370 if h.rmfile():
371 os.unlink(self.fname)
371 os.unlink(self.fname)
372 else:
372 else:
373 self.lines[:] = h.new()
373 self.lines[:] = h.new()
374 self.offset += len(h.new())
374 self.offset += len(h.new())
375 self.dirty = 1
375 self.dirty = 1
376 return 0
376 return 0
377
377
378 # fast case first, no offsets, no fuzz
378 # fast case first, no offsets, no fuzz
379 old = h.old()
379 old = h.old()
380 # patch starts counting at 1 unless we are adding the file
380 # patch starts counting at 1 unless we are adding the file
381 if h.starta == 0:
381 if h.starta == 0:
382 start = 0
382 start = 0
383 else:
383 else:
384 start = h.starta + self.offset - 1
384 start = h.starta + self.offset - 1
385 orig_start = start
385 orig_start = start
386 if diffhelpers.testhunk(old, self.lines, start) == 0:
386 if diffhelpers.testhunk(old, self.lines, start) == 0:
387 if h.rmfile():
387 if h.rmfile():
388 os.unlink(self.fname)
388 os.unlink(self.fname)
389 else:
389 else:
390 self.lines[start : start + h.lena] = h.new()
390 self.lines[start : start + h.lena] = h.new()
391 self.offset += h.lenb - h.lena
391 self.offset += h.lenb - h.lena
392 self.dirty = 1
392 self.dirty = 1
393 return 0
393 return 0
394
394
395 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
395 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
396 self.hashlines()
396 self.hashlines()
397 if h.hunk[-1][0] != ' ':
397 if h.hunk[-1][0] != ' ':
398 # if the hunk tried to put something at the bottom of the file
398 # if the hunk tried to put something at the bottom of the file
399 # override the start line and use eof here
399 # override the start line and use eof here
400 search_start = len(self.lines)
400 search_start = len(self.lines)
401 else:
401 else:
402 search_start = orig_start
402 search_start = orig_start
403
403
404 for fuzzlen in xrange(3):
404 for fuzzlen in xrange(3):
405 for toponly in [ True, False ]:
405 for toponly in [ True, False ]:
406 old = h.old(fuzzlen, toponly)
406 old = h.old(fuzzlen, toponly)
407
407
408 cand = self.findlines(old[0][1:], search_start)
408 cand = self.findlines(old[0][1:], search_start)
409 for l in cand:
409 for l in cand:
410 if diffhelpers.testhunk(old, self.lines, l) == 0:
410 if diffhelpers.testhunk(old, self.lines, l) == 0:
411 newlines = h.new(fuzzlen, toponly)
411 newlines = h.new(fuzzlen, toponly)
412 self.lines[l : l + len(old)] = newlines
412 self.lines[l : l + len(old)] = newlines
413 self.offset += len(newlines) - len(old)
413 self.offset += len(newlines) - len(old)
414 self.dirty = 1
414 self.dirty = 1
415 if fuzzlen:
415 if fuzzlen:
416 fuzzstr = "with fuzz %d " % fuzzlen
416 fuzzstr = "with fuzz %d " % fuzzlen
417 f = self.ui.warn
417 f = self.ui.warn
418 self.printfile(True)
418 self.printfile(True)
419 else:
419 else:
420 fuzzstr = ""
420 fuzzstr = ""
421 f = self.ui.note
421 f = self.ui.note
422 offset = l - orig_start - fuzzlen
422 offset = l - orig_start - fuzzlen
423 if offset == 1:
423 if offset == 1:
424 linestr = "line"
424 linestr = "line"
425 else:
425 else:
426 linestr = "lines"
426 linestr = "lines"
427 f(_("Hunk #%d succeeded at %d %s(offset %d %s).\n") %
427 f(_("Hunk #%d succeeded at %d %s(offset %d %s).\n") %
428 (h.number, l+1, fuzzstr, offset, linestr))
428 (h.number, l+1, fuzzstr, offset, linestr))
429 return fuzzlen
429 return fuzzlen
430 self.printfile(True)
430 self.printfile(True)
431 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
431 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
432 self.rej.append(h)
432 self.rej.append(h)
433 return -1
433 return -1
434
434
435 class hunk:
435 class hunk:
436 def __init__(self, desc, num, lr, context):
436 def __init__(self, desc, num, lr, context):
437 self.number = num
437 self.number = num
438 self.desc = desc
438 self.desc = desc
439 self.hunk = [ desc ]
439 self.hunk = [ desc ]
440 self.a = []
440 self.a = []
441 self.b = []
441 self.b = []
442 if context:
442 if context:
443 self.read_context_hunk(lr)
443 self.read_context_hunk(lr)
444 else:
444 else:
445 self.read_unified_hunk(lr)
445 self.read_unified_hunk(lr)
446
446
447 def read_unified_hunk(self, lr):
447 def read_unified_hunk(self, lr):
448 m = unidesc.match(self.desc)
448 m = unidesc.match(self.desc)
449 if not m:
449 if not m:
450 raise PatchError(_("bad hunk #%d") % self.number)
450 raise PatchError(_("bad hunk #%d") % self.number)
451 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
451 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
452 if self.lena == None:
452 if self.lena == None:
453 self.lena = 1
453 self.lena = 1
454 else:
454 else:
455 self.lena = int(self.lena)
455 self.lena = int(self.lena)
456 if self.lenb == None:
456 if self.lenb == None:
457 self.lenb = 1
457 self.lenb = 1
458 else:
458 else:
459 self.lenb = int(self.lenb)
459 self.lenb = int(self.lenb)
460 self.starta = int(self.starta)
460 self.starta = int(self.starta)
461 self.startb = int(self.startb)
461 self.startb = int(self.startb)
462 diffhelpers.addlines(lr.fp, self.hunk, self.lena, self.lenb, self.a, self.b)
462 diffhelpers.addlines(lr.fp, self.hunk, self.lena, self.lenb, self.a, self.b)
463 # if we hit eof before finishing out the hunk, the last line will
463 # if we hit eof before finishing out the hunk, the last line will
464 # be zero length. Lets try to fix it up.
464 # be zero length. Lets try to fix it up.
465 while len(self.hunk[-1]) == 0:
465 while len(self.hunk[-1]) == 0:
466 del self.hunk[-1]
466 del self.hunk[-1]
467 del self.a[-1]
467 del self.a[-1]
468 del self.b[-1]
468 del self.b[-1]
469 self.lena -= 1
469 self.lena -= 1
470 self.lenb -= 1
470 self.lenb -= 1
471
471
472 def read_context_hunk(self, lr):
472 def read_context_hunk(self, lr):
473 self.desc = lr.readline()
473 self.desc = lr.readline()
474 m = contextdesc.match(self.desc)
474 m = contextdesc.match(self.desc)
475 if not m:
475 if not m:
476 raise PatchError(_("bad hunk #%d") % self.number)
476 raise PatchError(_("bad hunk #%d") % self.number)
477 foo, self.starta, foo2, aend, foo3 = m.groups()
477 foo, self.starta, foo2, aend, foo3 = m.groups()
478 self.starta = int(self.starta)
478 self.starta = int(self.starta)
479 if aend == None:
479 if aend == None:
480 aend = self.starta
480 aend = self.starta
481 self.lena = int(aend) - self.starta
481 self.lena = int(aend) - self.starta
482 if self.starta:
482 if self.starta:
483 self.lena += 1
483 self.lena += 1
484 for x in xrange(self.lena):
484 for x in xrange(self.lena):
485 l = lr.readline()
485 l = lr.readline()
486 if l.startswith('---'):
486 if l.startswith('---'):
487 lr.push(l)
487 lr.push(l)
488 break
488 break
489 s = l[2:]
489 s = l[2:]
490 if l.startswith('- ') or l.startswith('! '):
490 if l.startswith('- ') or l.startswith('! '):
491 u = '-' + s
491 u = '-' + s
492 elif l.startswith(' '):
492 elif l.startswith(' '):
493 u = ' ' + s
493 u = ' ' + s
494 else:
494 else:
495 raise PatchError(_("bad hunk #%d old text line %d") %
495 raise PatchError(_("bad hunk #%d old text line %d") %
496 (self.number, x))
496 (self.number, x))
497 self.a.append(u)
497 self.a.append(u)
498 self.hunk.append(u)
498 self.hunk.append(u)
499
499
500 l = lr.readline()
500 l = lr.readline()
501 if l.startswith('\ '):
501 if l.startswith('\ '):
502 s = self.a[-1][:-1]
502 s = self.a[-1][:-1]
503 self.a[-1] = s
503 self.a[-1] = s
504 self.hunk[-1] = s
504 self.hunk[-1] = s
505 l = lr.readline()
505 l = lr.readline()
506 m = contextdesc.match(l)
506 m = contextdesc.match(l)
507 if not m:
507 if not m:
508 raise PatchError(_("bad hunk #%d") % self.number)
508 raise PatchError(_("bad hunk #%d") % self.number)
509 foo, self.startb, foo2, bend, foo3 = m.groups()
509 foo, self.startb, foo2, bend, foo3 = m.groups()
510 self.startb = int(self.startb)
510 self.startb = int(self.startb)
511 if bend == None:
511 if bend == None:
512 bend = self.startb
512 bend = self.startb
513 self.lenb = int(bend) - self.startb
513 self.lenb = int(bend) - self.startb
514 if self.startb:
514 if self.startb:
515 self.lenb += 1
515 self.lenb += 1
516 hunki = 1
516 hunki = 1
517 for x in xrange(self.lenb):
517 for x in xrange(self.lenb):
518 l = lr.readline()
518 l = lr.readline()
519 if l.startswith('\ '):
519 if l.startswith('\ '):
520 s = self.b[-1][:-1]
520 s = self.b[-1][:-1]
521 self.b[-1] = s
521 self.b[-1] = s
522 self.hunk[hunki-1] = s
522 self.hunk[hunki-1] = s
523 continue
523 continue
524 if not l:
524 if not l:
525 lr.push(l)
525 lr.push(l)
526 break
526 break
527 s = l[2:]
527 s = l[2:]
528 if l.startswith('+ ') or l.startswith('! '):
528 if l.startswith('+ ') or l.startswith('! '):
529 u = '+' + s
529 u = '+' + s
530 elif l.startswith(' '):
530 elif l.startswith(' '):
531 u = ' ' + s
531 u = ' ' + s
532 elif len(self.b) == 0:
532 elif len(self.b) == 0:
533 # this can happen when the hunk does not add any lines
533 # this can happen when the hunk does not add any lines
534 lr.push(l)
534 lr.push(l)
535 break
535 break
536 else:
536 else:
537 raise PatchError(_("bad hunk #%d old text line %d") %
537 raise PatchError(_("bad hunk #%d old text line %d") %
538 (self.number, x))
538 (self.number, x))
539 self.b.append(s)
539 self.b.append(s)
540 while True:
540 while True:
541 if hunki >= len(self.hunk):
541 if hunki >= len(self.hunk):
542 h = ""
542 h = ""
543 else:
543 else:
544 h = self.hunk[hunki]
544 h = self.hunk[hunki]
545 hunki += 1
545 hunki += 1
546 if h == u:
546 if h == u:
547 break
547 break
548 elif h.startswith('-'):
548 elif h.startswith('-'):
549 continue
549 continue
550 else:
550 else:
551 self.hunk.insert(hunki-1, u)
551 self.hunk.insert(hunki-1, u)
552 break
552 break
553
553
554 if not self.a:
554 if not self.a:
555 # this happens when lines were only added to the hunk
555 # this happens when lines were only added to the hunk
556 for x in self.hunk:
556 for x in self.hunk:
557 if x.startswith('-') or x.startswith(' '):
557 if x.startswith('-') or x.startswith(' '):
558 self.a.append(x)
558 self.a.append(x)
559 if not self.b:
559 if not self.b:
560 # this happens when lines were only deleted from the hunk
560 # this happens when lines were only deleted from the hunk
561 for x in self.hunk:
561 for x in self.hunk:
562 if x.startswith('+') or x.startswith(' '):
562 if x.startswith('+') or x.startswith(' '):
563 self.b.append(x[1:])
563 self.b.append(x[1:])
564 # @@ -start,len +start,len @@
564 # @@ -start,len +start,len @@
565 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
565 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
566 self.startb, self.lenb)
566 self.startb, self.lenb)
567 self.hunk[0] = self.desc
567 self.hunk[0] = self.desc
568
568
569 def reverse(self):
569 def reverse(self):
570 origlena = self.lena
570 origlena = self.lena
571 origstarta = self.starta
571 origstarta = self.starta
572 self.lena = self.lenb
572 self.lena = self.lenb
573 self.starta = self.startb
573 self.starta = self.startb
574 self.lenb = origlena
574 self.lenb = origlena
575 self.startb = origstarta
575 self.startb = origstarta
576 self.a = []
576 self.a = []
577 self.b = []
577 self.b = []
578 # self.hunk[0] is the @@ description
578 # self.hunk[0] is the @@ description
579 for x in xrange(1, len(self.hunk)):
579 for x in xrange(1, len(self.hunk)):
580 o = self.hunk[x]
580 o = self.hunk[x]
581 if o.startswith('-'):
581 if o.startswith('-'):
582 n = '+' + o[1:]
582 n = '+' + o[1:]
583 self.b.append(o[1:])
583 self.b.append(o[1:])
584 elif o.startswith('+'):
584 elif o.startswith('+'):
585 n = '-' + o[1:]
585 n = '-' + o[1:]
586 self.a.append(n)
586 self.a.append(n)
587 else:
587 else:
588 n = o
588 n = o
589 self.b.append(o[1:])
589 self.b.append(o[1:])
590 self.a.append(o)
590 self.a.append(o)
591 self.hunk[x] = o
591 self.hunk[x] = o
592
592
593 def fix_newline(self):
593 def fix_newline(self):
594 diffhelpers.fix_newline(self.hunk, self.a, self.b)
594 diffhelpers.fix_newline(self.hunk, self.a, self.b)
595
595
596 def complete(self):
596 def complete(self):
597 return len(self.a) == self.lena and len(self.b) == self.lenb
597 return len(self.a) == self.lena and len(self.b) == self.lenb
598
598
599 def createfile(self):
599 def createfile(self):
600 return self.starta == 0 and self.lena == 0
600 return self.starta == 0 and self.lena == 0
601
601
602 def rmfile(self):
602 def rmfile(self):
603 return self.startb == 0 and self.lenb == 0
603 return self.startb == 0 and self.lenb == 0
604
604
605 def fuzzit(self, l, fuzz, toponly):
605 def fuzzit(self, l, fuzz, toponly):
606 # this removes context lines from the top and bottom of list 'l'. It
606 # this removes context lines from the top and bottom of list 'l'. It
607 # checks the hunk to make sure only context lines are removed, and then
607 # checks the hunk to make sure only context lines are removed, and then
608 # returns a new shortened list of lines.
608 # returns a new shortened list of lines.
609 fuzz = min(fuzz, len(l)-1)
609 fuzz = min(fuzz, len(l)-1)
610 if fuzz:
610 if fuzz:
611 top = 0
611 top = 0
612 bot = 0
612 bot = 0
613 hlen = len(self.hunk)
613 hlen = len(self.hunk)
614 for x in xrange(hlen-1):
614 for x in xrange(hlen-1):
615 # the hunk starts with the @@ line, so use x+1
615 # the hunk starts with the @@ line, so use x+1
616 if self.hunk[x+1][0] == ' ':
616 if self.hunk[x+1][0] == ' ':
617 top += 1
617 top += 1
618 else:
618 else:
619 break
619 break
620 if not toponly:
620 if not toponly:
621 for x in xrange(hlen-1):
621 for x in xrange(hlen-1):
622 if self.hunk[hlen-bot-1][0] == ' ':
622 if self.hunk[hlen-bot-1][0] == ' ':
623 bot += 1
623 bot += 1
624 else:
624 else:
625 break
625 break
626
626
627 # top and bot now count context in the hunk
627 # top and bot now count context in the hunk
628 # adjust them if either one is short
628 # adjust them if either one is short
629 context = max(top, bot, 3)
629 context = max(top, bot, 3)
630 if bot < context:
630 if bot < context:
631 bot = max(0, fuzz - (context - bot))
631 bot = max(0, fuzz - (context - bot))
632 else:
632 else:
633 bot = min(fuzz, bot)
633 bot = min(fuzz, bot)
634 if top < context:
634 if top < context:
635 top = max(0, fuzz - (context - top))
635 top = max(0, fuzz - (context - top))
636 else:
636 else:
637 top = min(fuzz, top)
637 top = min(fuzz, top)
638
638
639 return l[top:len(l)-bot]
639 return l[top:len(l)-bot]
640 return l
640 return l
641
641
642 def old(self, fuzz=0, toponly=False):
642 def old(self, fuzz=0, toponly=False):
643 return self.fuzzit(self.a, fuzz, toponly)
643 return self.fuzzit(self.a, fuzz, toponly)
644
644
645 def newctrl(self):
645 def newctrl(self):
646 res = []
646 res = []
647 for x in self.hunk:
647 for x in self.hunk:
648 c = x[0]
648 c = x[0]
649 if c == ' ' or c == '+':
649 if c == ' ' or c == '+':
650 res.append(x)
650 res.append(x)
651 return res
651 return res
652
652
653 def new(self, fuzz=0, toponly=False):
653 def new(self, fuzz=0, toponly=False):
654 return self.fuzzit(self.b, fuzz, toponly)
654 return self.fuzzit(self.b, fuzz, toponly)
655
655
656 class binhunk:
656 class binhunk:
657 'A binary patch file. Only understands literals so far.'
657 'A binary patch file. Only understands literals so far.'
658 def __init__(self, gitpatch):
658 def __init__(self, gitpatch):
659 self.gitpatch = gitpatch
659 self.gitpatch = gitpatch
660 self.text = None
660 self.text = None
661 self.hunk = ['GIT binary patch\n']
661 self.hunk = ['GIT binary patch\n']
662
662
663 def createfile(self):
663 def createfile(self):
664 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
664 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
665
665
666 def rmfile(self):
666 def rmfile(self):
667 return self.gitpatch.op == 'DELETE'
667 return self.gitpatch.op == 'DELETE'
668
668
669 def complete(self):
669 def complete(self):
670 return self.text is not None
670 return self.text is not None
671
671
672 def new(self):
672 def new(self):
673 return [self.text]
673 return [self.text]
674
674
675 def extract(self, fp):
675 def extract(self, fp):
676 line = fp.readline()
676 line = fp.readline()
677 self.hunk.append(line)
677 self.hunk.append(line)
678 while line and not line.startswith('literal '):
678 while line and not line.startswith('literal '):
679 line = fp.readline()
679 line = fp.readline()
680 self.hunk.append(line)
680 self.hunk.append(line)
681 if not line:
681 if not line:
682 raise PatchError(_('could not extract binary patch'))
682 raise PatchError(_('could not extract binary patch'))
683 size = int(line[8:].rstrip())
683 size = int(line[8:].rstrip())
684 dec = []
684 dec = []
685 line = fp.readline()
685 line = fp.readline()
686 self.hunk.append(line)
686 self.hunk.append(line)
687 while len(line) > 1:
687 while len(line) > 1:
688 l = line[0]
688 l = line[0]
689 if l <= 'Z' and l >= 'A':
689 if l <= 'Z' and l >= 'A':
690 l = ord(l) - ord('A') + 1
690 l = ord(l) - ord('A') + 1
691 else:
691 else:
692 l = ord(l) - ord('a') + 27
692 l = ord(l) - ord('a') + 27
693 dec.append(base85.b85decode(line[1:-1])[:l])
693 dec.append(base85.b85decode(line[1:-1])[:l])
694 line = fp.readline()
694 line = fp.readline()
695 self.hunk.append(line)
695 self.hunk.append(line)
696 text = zlib.decompress(''.join(dec))
696 text = zlib.decompress(''.join(dec))
697 if len(text) != size:
697 if len(text) != size:
698 raise PatchError(_('binary patch is %d bytes, not %d') %
698 raise PatchError(_('binary patch is %d bytes, not %d') %
699 len(text), size)
699 len(text), size)
700 self.text = text
700 self.text = text
701
701
702 def parsefilename(str):
702 def parsefilename(str):
703 # --- filename \t|space stuff
703 # --- filename \t|space stuff
704 s = str[4:]
704 s = str[4:]
705 i = s.find('\t')
705 i = s.find('\t')
706 if i < 0:
706 if i < 0:
707 i = s.find(' ')
707 i = s.find(' ')
708 if i < 0:
708 if i < 0:
709 return s
709 return s
710 return s[:i]
710 return s[:i]
711
711
712 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse):
712 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse):
713 def pathstrip(path, count=1):
713 def pathstrip(path, count=1):
714 pathlen = len(path)
714 pathlen = len(path)
715 i = 0
715 i = 0
716 if count == 0:
716 if count == 0:
717 return path.rstrip()
717 return path.rstrip()
718 while count > 0:
718 while count > 0:
719 i = path.find(os.sep, i)
719 i = path.find(os.sep, i)
720 if i == -1:
720 if i == -1:
721 raise PatchError(_("unable to strip away %d dirs from %s") %
721 raise PatchError(_("unable to strip away %d dirs from %s") %
722 (count, path))
722 (count, path))
723 i += 1
723 i += 1
724 # consume '//' in the path
724 # consume '//' in the path
725 while i < pathlen - 1 and path[i] == os.sep:
725 while i < pathlen - 1 and path[i] == os.sep:
726 i += 1
726 i += 1
727 count -= 1
727 count -= 1
728 return path[i:].rstrip()
728 return path[i:].rstrip()
729
729
730 nulla = afile_orig == "/dev/null"
730 nulla = afile_orig == "/dev/null"
731 nullb = bfile_orig == "/dev/null"
731 nullb = bfile_orig == "/dev/null"
732 afile = pathstrip(afile_orig, strip)
732 afile = pathstrip(afile_orig, strip)
733 gooda = os.path.exists(afile) and not nulla
733 gooda = os.path.exists(afile) and not nulla
734 bfile = pathstrip(bfile_orig, strip)
734 bfile = pathstrip(bfile_orig, strip)
735 if afile == bfile:
735 if afile == bfile:
736 goodb = gooda
736 goodb = gooda
737 else:
737 else:
738 goodb = os.path.exists(bfile) and not nullb
738 goodb = os.path.exists(bfile) and not nullb
739 createfunc = hunk.createfile
739 createfunc = hunk.createfile
740 if reverse:
740 if reverse:
741 createfunc = hunk.rmfile
741 createfunc = hunk.rmfile
742 if not goodb and not gooda and not createfunc():
742 if not goodb and not gooda and not createfunc():
743 raise PatchError(_("unable to find %s or %s for patching") %
743 raise PatchError(_("unable to find %s or %s for patching") %
744 (afile, bfile))
744 (afile, bfile))
745 if gooda and goodb:
745 if gooda and goodb:
746 fname = bfile
746 fname = bfile
747 if afile in bfile:
747 if afile in bfile:
748 fname = afile
748 fname = afile
749 elif gooda:
749 elif gooda:
750 fname = afile
750 fname = afile
751 elif not nullb:
751 elif not nullb:
752 fname = bfile
752 fname = bfile
753 if afile in bfile:
753 if afile in bfile:
754 fname = afile
754 fname = afile
755 elif not nulla:
755 elif not nulla:
756 fname = afile
756 fname = afile
757 return fname
757 return fname
758
758
759 class linereader:
759 class linereader:
760 # simple class to allow pushing lines back into the input stream
760 # simple class to allow pushing lines back into the input stream
761 def __init__(self, fp):
761 def __init__(self, fp):
762 self.fp = fp
762 self.fp = fp
763 self.buf = []
763 self.buf = []
764
764
765 def push(self, line):
765 def push(self, line):
766 self.buf.append(line)
766 self.buf.append(line)
767
767
768 def readline(self):
768 def readline(self):
769 if self.buf:
769 if self.buf:
770 l = self.buf[0]
770 l = self.buf[0]
771 del self.buf[0]
771 del self.buf[0]
772 return l
772 return l
773 return self.fp.readline()
773 return self.fp.readline()
774
774
775 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
775 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
776 rejmerge=None, updatedir=None):
776 rejmerge=None, updatedir=None):
777 """reads a patch from fp and tries to apply it. The dict 'changed' is
777 """reads a patch from fp and tries to apply it. The dict 'changed' is
778 filled in with all of the filenames changed by the patch. Returns 0
778 filled in with all of the filenames changed by the patch. Returns 0
779 for a clean patch, -1 if any rejects were found and 1 if there was
779 for a clean patch, -1 if any rejects were found and 1 if there was
780 any fuzz."""
780 any fuzz."""
781
781
782 def scangitpatch(fp, firstline, cwd=None):
782 def scangitpatch(fp, firstline, cwd=None):
783 '''git patches can modify a file, then copy that file to
783 '''git patches can modify a file, then copy that file to
784 a new file, but expect the source to be the unmodified form.
784 a new file, but expect the source to be the unmodified form.
785 So we scan the patch looking for that case so we can do
785 So we scan the patch looking for that case so we can do
786 the copies ahead of time.'''
786 the copies ahead of time.'''
787
787
788 pos = 0
788 pos = 0
789 try:
789 try:
790 pos = fp.tell()
790 pos = fp.tell()
791 except IOError:
791 except IOError:
792 fp = cStringIO.StringIO(fp.read())
792 fp = cStringIO.StringIO(fp.read())
793
793
794 (dopatch, gitpatches) = readgitpatch(fp, firstline)
794 (dopatch, gitpatches) = readgitpatch(fp, firstline)
795 for gp in gitpatches:
795 for gp in gitpatches:
796 if gp.copymod:
796 if gp.copymod:
797 copyfile(gp.oldpath, gp.path, basedir=cwd)
797 copyfile(gp.oldpath, gp.path, basedir=cwd)
798
798
799 fp.seek(pos)
799 fp.seek(pos)
800
800
801 return fp, dopatch, gitpatches
801 return fp, dopatch, gitpatches
802
802
803 current_hunk = None
803 current_hunk = None
804 current_file = None
804 current_file = None
805 afile = ""
805 afile = ""
806 bfile = ""
806 bfile = ""
807 state = None
807 state = None
808 hunknum = 0
808 hunknum = 0
809 rejects = 0
809 rejects = 0
810
810
811 git = False
811 git = False
812 gitre = re.compile('diff --git (a/.*) (b/.*)')
812 gitre = re.compile('diff --git (a/.*) (b/.*)')
813
813
814 # our states
814 # our states
815 BFILE = 1
815 BFILE = 1
816 err = 0
816 err = 0
817 context = None
817 context = None
818 lr = linereader(fp)
818 lr = linereader(fp)
819 dopatch = True
819 dopatch = True
820 gitworkdone = False
820 gitworkdone = False
821
821
822 while True:
822 while True:
823 newfile = False
823 newfile = False
824 x = lr.readline()
824 x = lr.readline()
825 if not x:
825 if not x:
826 break
826 break
827 if current_hunk:
827 if current_hunk:
828 if x.startswith('\ '):
828 if x.startswith('\ '):
829 current_hunk.fix_newline()
829 current_hunk.fix_newline()
830 ret = current_file.apply(current_hunk, reverse)
830 ret = current_file.apply(current_hunk, reverse)
831 if ret > 0:
831 if ret >= 0:
832 err = 1
832 changed.setdefault(current_file.fname, (None, None))
833 if ret > 0:
834 err = 1
833 current_hunk = None
835 current_hunk = None
834 gitworkdone = False
836 gitworkdone = False
835 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
837 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
836 ((context or context == None) and x.startswith('***************')))):
838 ((context or context == None) and x.startswith('***************')))):
837 try:
839 try:
838 if context == None and x.startswith('***************'):
840 if context == None and x.startswith('***************'):
839 context = True
841 context = True
840 current_hunk = hunk(x, hunknum + 1, lr, context)
842 current_hunk = hunk(x, hunknum + 1, lr, context)
841 except PatchError, err:
843 except PatchError, err:
842 ui.debug(err)
844 ui.debug(err)
843 current_hunk = None
845 current_hunk = None
844 continue
846 continue
845 hunknum += 1
847 hunknum += 1
846 if not current_file:
848 if not current_file:
847 if sourcefile:
849 if sourcefile:
848 current_file = patchfile(ui, sourcefile)
850 current_file = patchfile(ui, sourcefile)
849 else:
851 else:
850 current_file = selectfile(afile, bfile, current_hunk,
852 current_file = selectfile(afile, bfile, current_hunk,
851 strip, reverse)
853 strip, reverse)
852 current_file = patchfile(ui, current_file)
854 current_file = patchfile(ui, current_file)
853 changed.setdefault(current_file.fname, (None, None))
854 elif state == BFILE and x.startswith('GIT binary patch'):
855 elif state == BFILE and x.startswith('GIT binary patch'):
855 current_hunk = binhunk(changed[bfile[2:]][1])
856 current_hunk = binhunk(changed[bfile[2:]][1])
856 if not current_file:
857 if not current_file:
857 if sourcefile:
858 if sourcefile:
858 current_file = patchfile(ui, sourcefile)
859 current_file = patchfile(ui, sourcefile)
859 else:
860 else:
860 current_file = selectfile(afile, bfile, current_hunk,
861 current_file = selectfile(afile, bfile, current_hunk,
861 strip, reverse)
862 strip, reverse)
862 current_file = patchfile(ui, current_file)
863 current_file = patchfile(ui, current_file)
863 hunknum += 1
864 hunknum += 1
864 current_hunk.extract(fp)
865 current_hunk.extract(fp)
865 elif x.startswith('diff --git'):
866 elif x.startswith('diff --git'):
866 # check for git diff, scanning the whole patch file if needed
867 # check for git diff, scanning the whole patch file if needed
867 m = gitre.match(x)
868 m = gitre.match(x)
868 if m:
869 if m:
869 afile, bfile = m.group(1, 2)
870 afile, bfile = m.group(1, 2)
870 if not git:
871 if not git:
871 git = True
872 git = True
872 fp, dopatch, gitpatches = scangitpatch(fp, x)
873 fp, dopatch, gitpatches = scangitpatch(fp, x)
873 for gp in gitpatches:
874 for gp in gitpatches:
874 changed[gp.path] = (gp.op, gp)
875 changed[gp.path] = (gp.op, gp)
875 # else error?
876 # else error?
876 # copy/rename + modify should modify target, not source
877 # copy/rename + modify should modify target, not source
877 if changed.get(bfile[2:], (None, None))[0] in ('COPY',
878 if changed.get(bfile[2:], (None, None))[0] in ('COPY',
878 'RENAME'):
879 'RENAME'):
879 afile = bfile
880 afile = bfile
880 gitworkdone = True
881 gitworkdone = True
881 newfile = True
882 newfile = True
882 elif x.startswith('---'):
883 elif x.startswith('---'):
883 # check for a unified diff
884 # check for a unified diff
884 l2 = lr.readline()
885 l2 = lr.readline()
885 if not l2.startswith('+++'):
886 if not l2.startswith('+++'):
886 lr.push(l2)
887 lr.push(l2)
887 continue
888 continue
888 newfile = True
889 newfile = True
889 context = False
890 context = False
890 afile = parsefilename(x)
891 afile = parsefilename(x)
891 bfile = parsefilename(l2)
892 bfile = parsefilename(l2)
892 elif x.startswith('***'):
893 elif x.startswith('***'):
893 # check for a context diff
894 # check for a context diff
894 l2 = lr.readline()
895 l2 = lr.readline()
895 if not l2.startswith('---'):
896 if not l2.startswith('---'):
896 lr.push(l2)
897 lr.push(l2)
897 continue
898 continue
898 l3 = lr.readline()
899 l3 = lr.readline()
899 lr.push(l3)
900 lr.push(l3)
900 if not l3.startswith("***************"):
901 if not l3.startswith("***************"):
901 lr.push(l2)
902 lr.push(l2)
902 continue
903 continue
903 newfile = True
904 newfile = True
904 context = True
905 context = True
905 afile = parsefilename(x)
906 afile = parsefilename(x)
906 bfile = parsefilename(l2)
907 bfile = parsefilename(l2)
907
908
908 if newfile:
909 if newfile:
909 if current_file:
910 if current_file:
910 current_file.close()
911 current_file.close()
911 if rejmerge:
912 if rejmerge:
912 rejmerge(current_file)
913 rejmerge(current_file)
913 rejects += len(current_file.rej)
914 rejects += len(current_file.rej)
914 state = BFILE
915 state = BFILE
915 current_file = None
916 current_file = None
916 hunknum = 0
917 hunknum = 0
917 if current_hunk:
918 if current_hunk:
918 if current_hunk.complete():
919 if current_hunk.complete():
919 ret = current_file.apply(current_hunk, reverse)
920 ret = current_file.apply(current_hunk, reverse)
920 if ret > 0:
921 if ret >= 0:
921 err = 1
922 changed.setdefault(current_file.fname, (None, None))
923 if ret > 0:
924 err = 1
922 else:
925 else:
923 fname = current_file and current_file.fname or None
926 fname = current_file and current_file.fname or None
924 raise PatchError(_("malformed patch %s %s") % (fname,
927 raise PatchError(_("malformed patch %s %s") % (fname,
925 current_hunk.desc))
928 current_hunk.desc))
926 if current_file:
929 if current_file:
927 current_file.close()
930 current_file.close()
928 if rejmerge:
931 if rejmerge:
929 rejmerge(current_file)
932 rejmerge(current_file)
930 rejects += len(current_file.rej)
933 rejects += len(current_file.rej)
931 if updatedir and git:
934 if updatedir and git:
932 updatedir(gitpatches)
935 updatedir(gitpatches)
933 if rejects:
936 if rejects:
934 return -1
937 return -1
935 if hunknum == 0 and dopatch and not gitworkdone:
938 if hunknum == 0 and dopatch and not gitworkdone:
936 raise PatchError(_("no valid hunks found"))
939 raise PatchError(_("no valid hunks found"))
937 return err
940 return err
938
941
939 def diffopts(ui, opts={}, untrusted=False):
942 def diffopts(ui, opts={}, untrusted=False):
940 def get(key, name=None):
943 def get(key, name=None):
941 return (opts.get(key) or
944 return (opts.get(key) or
942 ui.configbool('diff', name or key, None, untrusted=untrusted))
945 ui.configbool('diff', name or key, None, untrusted=untrusted))
943 return mdiff.diffopts(
946 return mdiff.diffopts(
944 text=opts.get('text'),
947 text=opts.get('text'),
945 git=get('git'),
948 git=get('git'),
946 nodates=get('nodates'),
949 nodates=get('nodates'),
947 showfunc=get('show_function', 'showfunc'),
950 showfunc=get('show_function', 'showfunc'),
948 ignorews=get('ignore_all_space', 'ignorews'),
951 ignorews=get('ignore_all_space', 'ignorews'),
949 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
952 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
950 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
953 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
951
954
952 def updatedir(ui, repo, patches, wlock=None):
955 def updatedir(ui, repo, patches, wlock=None):
953 '''Update dirstate after patch application according to metadata'''
956 '''Update dirstate after patch application according to metadata'''
954 if not patches:
957 if not patches:
955 return
958 return
956 copies = []
959 copies = []
957 removes = {}
960 removes = {}
958 cfiles = patches.keys()
961 cfiles = patches.keys()
959 cwd = repo.getcwd()
962 cwd = repo.getcwd()
960 if cwd:
963 if cwd:
961 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
964 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
962 for f in patches:
965 for f in patches:
963 ctype, gp = patches[f]
966 ctype, gp = patches[f]
964 if ctype == 'RENAME':
967 if ctype == 'RENAME':
965 copies.append((gp.oldpath, gp.path, gp.copymod))
968 copies.append((gp.oldpath, gp.path, gp.copymod))
966 removes[gp.oldpath] = 1
969 removes[gp.oldpath] = 1
967 elif ctype == 'COPY':
970 elif ctype == 'COPY':
968 copies.append((gp.oldpath, gp.path, gp.copymod))
971 copies.append((gp.oldpath, gp.path, gp.copymod))
969 elif ctype == 'DELETE':
972 elif ctype == 'DELETE':
970 removes[gp.path] = 1
973 removes[gp.path] = 1
971 for src, dst, after in copies:
974 for src, dst, after in copies:
972 if not after:
975 if not after:
973 copyfile(src, dst, repo.root)
976 copyfile(src, dst, repo.root)
974 repo.copy(src, dst, wlock=wlock)
977 repo.copy(src, dst, wlock=wlock)
975 removes = removes.keys()
978 removes = removes.keys()
976 if removes:
979 if removes:
977 removes.sort()
980 removes.sort()
978 repo.remove(removes, True, wlock=wlock)
981 repo.remove(removes, True, wlock=wlock)
979 for f in patches:
982 for f in patches:
980 ctype, gp = patches[f]
983 ctype, gp = patches[f]
981 if gp and gp.mode:
984 if gp and gp.mode:
982 x = gp.mode & 0100 != 0
985 x = gp.mode & 0100 != 0
983 dst = os.path.join(repo.root, gp.path)
986 dst = os.path.join(repo.root, gp.path)
984 # patch won't create empty files
987 # patch won't create empty files
985 if ctype == 'ADD' and not os.path.exists(dst):
988 if ctype == 'ADD' and not os.path.exists(dst):
986 repo.wwrite(gp.path, '', x and 'x' or '')
989 repo.wwrite(gp.path, '', x and 'x' or '')
987 else:
990 else:
988 util.set_exec(dst, x)
991 util.set_exec(dst, x)
989 cmdutil.addremove(repo, cfiles, wlock=wlock)
992 cmdutil.addremove(repo, cfiles, wlock=wlock)
990 files = patches.keys()
993 files = patches.keys()
991 files.extend([r for r in removes if r not in files])
994 files.extend([r for r in removes if r not in files])
992 files.sort()
995 files.sort()
993
996
994 return files
997 return files
995
998
996 def b85diff(fp, to, tn):
999 def b85diff(fp, to, tn):
997 '''print base85-encoded binary diff'''
1000 '''print base85-encoded binary diff'''
998 def gitindex(text):
1001 def gitindex(text):
999 if not text:
1002 if not text:
1000 return '0' * 40
1003 return '0' * 40
1001 l = len(text)
1004 l = len(text)
1002 s = sha.new('blob %d\0' % l)
1005 s = sha.new('blob %d\0' % l)
1003 s.update(text)
1006 s.update(text)
1004 return s.hexdigest()
1007 return s.hexdigest()
1005
1008
1006 def fmtline(line):
1009 def fmtline(line):
1007 l = len(line)
1010 l = len(line)
1008 if l <= 26:
1011 if l <= 26:
1009 l = chr(ord('A') + l - 1)
1012 l = chr(ord('A') + l - 1)
1010 else:
1013 else:
1011 l = chr(l - 26 + ord('a') - 1)
1014 l = chr(l - 26 + ord('a') - 1)
1012 return '%c%s\n' % (l, base85.b85encode(line, True))
1015 return '%c%s\n' % (l, base85.b85encode(line, True))
1013
1016
1014 def chunk(text, csize=52):
1017 def chunk(text, csize=52):
1015 l = len(text)
1018 l = len(text)
1016 i = 0
1019 i = 0
1017 while i < l:
1020 while i < l:
1018 yield text[i:i+csize]
1021 yield text[i:i+csize]
1019 i += csize
1022 i += csize
1020
1023
1021 tohash = gitindex(to)
1024 tohash = gitindex(to)
1022 tnhash = gitindex(tn)
1025 tnhash = gitindex(tn)
1023 if tohash == tnhash:
1026 if tohash == tnhash:
1024 return ""
1027 return ""
1025
1028
1026 # TODO: deltas
1029 # TODO: deltas
1027 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1030 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1028 (tohash, tnhash, len(tn))]
1031 (tohash, tnhash, len(tn))]
1029 for l in chunk(zlib.compress(tn)):
1032 for l in chunk(zlib.compress(tn)):
1030 ret.append(fmtline(l))
1033 ret.append(fmtline(l))
1031 ret.append('\n')
1034 ret.append('\n')
1032 return ''.join(ret)
1035 return ''.join(ret)
1033
1036
1034 def diff(repo, node1=None, node2=None, files=None, match=util.always,
1037 def diff(repo, node1=None, node2=None, files=None, match=util.always,
1035 fp=None, changes=None, opts=None):
1038 fp=None, changes=None, opts=None):
1036 '''print diff of changes to files between two nodes, or node and
1039 '''print diff of changes to files between two nodes, or node and
1037 working directory.
1040 working directory.
1038
1041
1039 if node1 is None, use first dirstate parent instead.
1042 if node1 is None, use first dirstate parent instead.
1040 if node2 is None, compare node1 with working directory.'''
1043 if node2 is None, compare node1 with working directory.'''
1041
1044
1042 if opts is None:
1045 if opts is None:
1043 opts = mdiff.defaultopts
1046 opts = mdiff.defaultopts
1044 if fp is None:
1047 if fp is None:
1045 fp = repo.ui
1048 fp = repo.ui
1046
1049
1047 if not node1:
1050 if not node1:
1048 node1 = repo.dirstate.parents()[0]
1051 node1 = repo.dirstate.parents()[0]
1049
1052
1050 ccache = {}
1053 ccache = {}
1051 def getctx(r):
1054 def getctx(r):
1052 if r not in ccache:
1055 if r not in ccache:
1053 ccache[r] = context.changectx(repo, r)
1056 ccache[r] = context.changectx(repo, r)
1054 return ccache[r]
1057 return ccache[r]
1055
1058
1056 flcache = {}
1059 flcache = {}
1057 def getfilectx(f, ctx):
1060 def getfilectx(f, ctx):
1058 flctx = ctx.filectx(f, filelog=flcache.get(f))
1061 flctx = ctx.filectx(f, filelog=flcache.get(f))
1059 if f not in flcache:
1062 if f not in flcache:
1060 flcache[f] = flctx._filelog
1063 flcache[f] = flctx._filelog
1061 return flctx
1064 return flctx
1062
1065
1063 # reading the data for node1 early allows it to play nicely
1066 # reading the data for node1 early allows it to play nicely
1064 # with repo.status and the revlog cache.
1067 # with repo.status and the revlog cache.
1065 ctx1 = context.changectx(repo, node1)
1068 ctx1 = context.changectx(repo, node1)
1066 # force manifest reading
1069 # force manifest reading
1067 man1 = ctx1.manifest()
1070 man1 = ctx1.manifest()
1068 date1 = util.datestr(ctx1.date())
1071 date1 = util.datestr(ctx1.date())
1069
1072
1070 if not changes:
1073 if not changes:
1071 changes = repo.status(node1, node2, files, match=match)[:5]
1074 changes = repo.status(node1, node2, files, match=match)[:5]
1072 modified, added, removed, deleted, unknown = changes
1075 modified, added, removed, deleted, unknown = changes
1073
1076
1074 if not modified and not added and not removed:
1077 if not modified and not added and not removed:
1075 return
1078 return
1076
1079
1077 if node2:
1080 if node2:
1078 ctx2 = context.changectx(repo, node2)
1081 ctx2 = context.changectx(repo, node2)
1079 execf2 = ctx2.manifest().execf
1082 execf2 = ctx2.manifest().execf
1080 else:
1083 else:
1081 ctx2 = context.workingctx(repo)
1084 ctx2 = context.workingctx(repo)
1082 execf2 = util.execfunc(repo.root, None)
1085 execf2 = util.execfunc(repo.root, None)
1083 if execf2 is None:
1086 if execf2 is None:
1084 execf2 = ctx2.parents()[0].manifest().copy().execf
1087 execf2 = ctx2.parents()[0].manifest().copy().execf
1085
1088
1086 # returns False if there was no rename between ctx1 and ctx2
1089 # returns False if there was no rename between ctx1 and ctx2
1087 # returns None if the file was created between ctx1 and ctx2
1090 # returns None if the file was created between ctx1 and ctx2
1088 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
1091 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
1089 def renamed(f):
1092 def renamed(f):
1090 startrev = ctx1.rev()
1093 startrev = ctx1.rev()
1091 c = ctx2
1094 c = ctx2
1092 crev = c.rev()
1095 crev = c.rev()
1093 if crev is None:
1096 if crev is None:
1094 crev = repo.changelog.count()
1097 crev = repo.changelog.count()
1095 orig = f
1098 orig = f
1096 while crev > startrev:
1099 while crev > startrev:
1097 if f in c.files():
1100 if f in c.files():
1098 try:
1101 try:
1099 src = getfilectx(f, c).renamed()
1102 src = getfilectx(f, c).renamed()
1100 except revlog.LookupError:
1103 except revlog.LookupError:
1101 return None
1104 return None
1102 if src:
1105 if src:
1103 f = src[0]
1106 f = src[0]
1104 crev = c.parents()[0].rev()
1107 crev = c.parents()[0].rev()
1105 # try to reuse
1108 # try to reuse
1106 c = getctx(crev)
1109 c = getctx(crev)
1107 if f not in man1:
1110 if f not in man1:
1108 return None
1111 return None
1109 if f == orig:
1112 if f == orig:
1110 return False
1113 return False
1111 return f
1114 return f
1112
1115
1113 if repo.ui.quiet:
1116 if repo.ui.quiet:
1114 r = None
1117 r = None
1115 else:
1118 else:
1116 hexfunc = repo.ui.debugflag and hex or short
1119 hexfunc = repo.ui.debugflag and hex or short
1117 r = [hexfunc(node) for node in [node1, node2] if node]
1120 r = [hexfunc(node) for node in [node1, node2] if node]
1118
1121
1119 if opts.git:
1122 if opts.git:
1120 copied = {}
1123 copied = {}
1121 for f in added:
1124 for f in added:
1122 src = renamed(f)
1125 src = renamed(f)
1123 if src:
1126 if src:
1124 copied[f] = src
1127 copied[f] = src
1125 srcs = [x[1] for x in copied.items()]
1128 srcs = [x[1] for x in copied.items()]
1126
1129
1127 all = modified + added + removed
1130 all = modified + added + removed
1128 all.sort()
1131 all.sort()
1129 gone = {}
1132 gone = {}
1130
1133
1131 for f in all:
1134 for f in all:
1132 to = None
1135 to = None
1133 tn = None
1136 tn = None
1134 dodiff = True
1137 dodiff = True
1135 header = []
1138 header = []
1136 if f in man1:
1139 if f in man1:
1137 to = getfilectx(f, ctx1).data()
1140 to = getfilectx(f, ctx1).data()
1138 if f not in removed:
1141 if f not in removed:
1139 tn = getfilectx(f, ctx2).data()
1142 tn = getfilectx(f, ctx2).data()
1140 if opts.git:
1143 if opts.git:
1141 def gitmode(x):
1144 def gitmode(x):
1142 return x and '100755' or '100644'
1145 return x and '100755' or '100644'
1143 def addmodehdr(header, omode, nmode):
1146 def addmodehdr(header, omode, nmode):
1144 if omode != nmode:
1147 if omode != nmode:
1145 header.append('old mode %s\n' % omode)
1148 header.append('old mode %s\n' % omode)
1146 header.append('new mode %s\n' % nmode)
1149 header.append('new mode %s\n' % nmode)
1147
1150
1148 a, b = f, f
1151 a, b = f, f
1149 if f in added:
1152 if f in added:
1150 mode = gitmode(execf2(f))
1153 mode = gitmode(execf2(f))
1151 if f in copied:
1154 if f in copied:
1152 a = copied[f]
1155 a = copied[f]
1153 omode = gitmode(man1.execf(a))
1156 omode = gitmode(man1.execf(a))
1154 addmodehdr(header, omode, mode)
1157 addmodehdr(header, omode, mode)
1155 if a in removed and a not in gone:
1158 if a in removed and a not in gone:
1156 op = 'rename'
1159 op = 'rename'
1157 gone[a] = 1
1160 gone[a] = 1
1158 else:
1161 else:
1159 op = 'copy'
1162 op = 'copy'
1160 header.append('%s from %s\n' % (op, a))
1163 header.append('%s from %s\n' % (op, a))
1161 header.append('%s to %s\n' % (op, f))
1164 header.append('%s to %s\n' % (op, f))
1162 to = getfilectx(a, ctx1).data()
1165 to = getfilectx(a, ctx1).data()
1163 else:
1166 else:
1164 header.append('new file mode %s\n' % mode)
1167 header.append('new file mode %s\n' % mode)
1165 if util.binary(tn):
1168 if util.binary(tn):
1166 dodiff = 'binary'
1169 dodiff = 'binary'
1167 elif f in removed:
1170 elif f in removed:
1168 if f in srcs:
1171 if f in srcs:
1169 dodiff = False
1172 dodiff = False
1170 else:
1173 else:
1171 mode = gitmode(man1.execf(f))
1174 mode = gitmode(man1.execf(f))
1172 header.append('deleted file mode %s\n' % mode)
1175 header.append('deleted file mode %s\n' % mode)
1173 else:
1176 else:
1174 omode = gitmode(man1.execf(f))
1177 omode = gitmode(man1.execf(f))
1175 nmode = gitmode(execf2(f))
1178 nmode = gitmode(execf2(f))
1176 addmodehdr(header, omode, nmode)
1179 addmodehdr(header, omode, nmode)
1177 if util.binary(to) or util.binary(tn):
1180 if util.binary(to) or util.binary(tn):
1178 dodiff = 'binary'
1181 dodiff = 'binary'
1179 r = None
1182 r = None
1180 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
1183 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
1181 if dodiff:
1184 if dodiff:
1182 if dodiff == 'binary':
1185 if dodiff == 'binary':
1183 text = b85diff(fp, to, tn)
1186 text = b85diff(fp, to, tn)
1184 else:
1187 else:
1185 text = mdiff.unidiff(to, date1,
1188 text = mdiff.unidiff(to, date1,
1186 # ctx2 date may be dynamic
1189 # ctx2 date may be dynamic
1187 tn, util.datestr(ctx2.date()),
1190 tn, util.datestr(ctx2.date()),
1188 f, r, opts=opts)
1191 f, r, opts=opts)
1189 if text or len(header) > 1:
1192 if text or len(header) > 1:
1190 fp.write(''.join(header))
1193 fp.write(''.join(header))
1191 fp.write(text)
1194 fp.write(text)
1192
1195
1193 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1196 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1194 opts=None):
1197 opts=None):
1195 '''export changesets as hg patches.'''
1198 '''export changesets as hg patches.'''
1196
1199
1197 total = len(revs)
1200 total = len(revs)
1198 revwidth = max([len(str(rev)) for rev in revs])
1201 revwidth = max([len(str(rev)) for rev in revs])
1199
1202
1200 def single(rev, seqno, fp):
1203 def single(rev, seqno, fp):
1201 ctx = repo.changectx(rev)
1204 ctx = repo.changectx(rev)
1202 node = ctx.node()
1205 node = ctx.node()
1203 parents = [p.node() for p in ctx.parents() if p]
1206 parents = [p.node() for p in ctx.parents() if p]
1204 branch = ctx.branch()
1207 branch = ctx.branch()
1205 if switch_parent:
1208 if switch_parent:
1206 parents.reverse()
1209 parents.reverse()
1207 prev = (parents and parents[0]) or nullid
1210 prev = (parents and parents[0]) or nullid
1208
1211
1209 if not fp:
1212 if not fp:
1210 fp = cmdutil.make_file(repo, template, node, total=total,
1213 fp = cmdutil.make_file(repo, template, node, total=total,
1211 seqno=seqno, revwidth=revwidth)
1214 seqno=seqno, revwidth=revwidth)
1212 if fp != sys.stdout and hasattr(fp, 'name'):
1215 if fp != sys.stdout and hasattr(fp, 'name'):
1213 repo.ui.note("%s\n" % fp.name)
1216 repo.ui.note("%s\n" % fp.name)
1214
1217
1215 fp.write("# HG changeset patch\n")
1218 fp.write("# HG changeset patch\n")
1216 fp.write("# User %s\n" % ctx.user())
1219 fp.write("# User %s\n" % ctx.user())
1217 fp.write("# Date %d %d\n" % ctx.date())
1220 fp.write("# Date %d %d\n" % ctx.date())
1218 if branch and (branch != 'default'):
1221 if branch and (branch != 'default'):
1219 fp.write("# Branch %s\n" % branch)
1222 fp.write("# Branch %s\n" % branch)
1220 fp.write("# Node ID %s\n" % hex(node))
1223 fp.write("# Node ID %s\n" % hex(node))
1221 fp.write("# Parent %s\n" % hex(prev))
1224 fp.write("# Parent %s\n" % hex(prev))
1222 if len(parents) > 1:
1225 if len(parents) > 1:
1223 fp.write("# Parent %s\n" % hex(parents[1]))
1226 fp.write("# Parent %s\n" % hex(parents[1]))
1224 fp.write(ctx.description().rstrip())
1227 fp.write(ctx.description().rstrip())
1225 fp.write("\n\n")
1228 fp.write("\n\n")
1226
1229
1227 diff(repo, prev, node, fp=fp, opts=opts)
1230 diff(repo, prev, node, fp=fp, opts=opts)
1228 if fp not in (sys.stdout, repo.ui):
1231 if fp not in (sys.stdout, repo.ui):
1229 fp.close()
1232 fp.close()
1230
1233
1231 for seqno, rev in enumerate(revs):
1234 for seqno, rev in enumerate(revs):
1232 single(rev, seqno+1, fp)
1235 single(rev, seqno+1, fp)
1233
1236
1234 def diffstat(patchlines):
1237 def diffstat(patchlines):
1235 if not util.find_exe('diffstat'):
1238 if not util.find_exe('diffstat'):
1236 return
1239 return
1237 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
1240 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
1238 try:
1241 try:
1239 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
1242 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
1240 try:
1243 try:
1241 for line in patchlines: print >> p.tochild, line
1244 for line in patchlines: print >> p.tochild, line
1242 p.tochild.close()
1245 p.tochild.close()
1243 if p.wait(): return
1246 if p.wait(): return
1244 fp = os.fdopen(fd, 'r')
1247 fp = os.fdopen(fd, 'r')
1245 stat = []
1248 stat = []
1246 for line in fp: stat.append(line.lstrip())
1249 for line in fp: stat.append(line.lstrip())
1247 last = stat.pop()
1250 last = stat.pop()
1248 stat.insert(0, last)
1251 stat.insert(0, last)
1249 stat = ''.join(stat)
1252 stat = ''.join(stat)
1250 if stat.startswith('0 files'): raise ValueError
1253 if stat.startswith('0 files'): raise ValueError
1251 return stat
1254 return stat
1252 except: raise
1255 except: raise
1253 finally:
1256 finally:
1254 try: os.unlink(name)
1257 try: os.unlink(name)
1255 except: pass
1258 except: pass
General Comments 0
You need to be logged in to leave comments. Login now