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