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