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