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