##// END OF EJS Templates
git binary patches: use hashes to detect identical files
Alexis S. L. Carvalho -
r4105:ed46895a default
parent child Browse files
Show More
@@ -1,682 +1,683
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 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 from demandload import demandload
8 from demandload import demandload
9 from i18n import gettext as _
9 from i18n import gettext as _
10 from node import *
10 from node import *
11 demandload(globals(), "base85 cmdutil mdiff util")
11 demandload(globals(), "base85 cmdutil mdiff util")
12 demandload(globals(), "cStringIO email.Parser errno os popen2 re shutil sha")
12 demandload(globals(), "cStringIO email.Parser errno os popen2 re shutil sha")
13 demandload(globals(), "sys tempfile zlib")
13 demandload(globals(), "sys tempfile zlib")
14
14
15 # helper functions
15 # helper functions
16
16
17 def copyfile(src, dst, basedir=None):
17 def copyfile(src, dst, basedir=None):
18 if not basedir:
18 if not basedir:
19 basedir = os.getcwd()
19 basedir = os.getcwd()
20
20
21 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
21 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
22 if os.path.exists(absdst):
22 if os.path.exists(absdst):
23 raise util.Abort(_("cannot create %s: destination already exists") %
23 raise util.Abort(_("cannot create %s: destination already exists") %
24 dst)
24 dst)
25
25
26 targetdir = os.path.dirname(absdst)
26 targetdir = os.path.dirname(absdst)
27 if not os.path.isdir(targetdir):
27 if not os.path.isdir(targetdir):
28 os.makedirs(targetdir)
28 os.makedirs(targetdir)
29
29
30 util.copyfile(abssrc, absdst)
30 util.copyfile(abssrc, absdst)
31
31
32 # public functions
32 # public functions
33
33
34 def extract(ui, fileobj):
34 def extract(ui, fileobj):
35 '''extract patch from data read from fileobj.
35 '''extract patch from data read from fileobj.
36
36
37 patch can be normal patch or contained in email message.
37 patch can be normal patch or contained in email message.
38
38
39 return tuple (filename, message, user, date). any item in returned
39 return tuple (filename, message, user, date). any item in returned
40 tuple can be None. if filename is None, fileobj did not contain
40 tuple can be None. if filename is None, fileobj did not contain
41 patch. caller must unlink filename when done.'''
41 patch. caller must unlink filename when done.'''
42
42
43 # attempt to detect the start of a patch
43 # attempt to detect the start of a patch
44 # (this heuristic is borrowed from quilt)
44 # (this heuristic is borrowed from quilt)
45 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
45 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
46 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
46 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
47 '(---|\*\*\*)[ \t])', re.MULTILINE)
47 '(---|\*\*\*)[ \t])', re.MULTILINE)
48
48
49 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
49 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
50 tmpfp = os.fdopen(fd, 'w')
50 tmpfp = os.fdopen(fd, 'w')
51 try:
51 try:
52 hgpatch = False
52 hgpatch = False
53
53
54 msg = email.Parser.Parser().parse(fileobj)
54 msg = email.Parser.Parser().parse(fileobj)
55
55
56 message = msg['Subject']
56 message = msg['Subject']
57 user = msg['From']
57 user = msg['From']
58 # should try to parse msg['Date']
58 # should try to parse msg['Date']
59 date = None
59 date = None
60
60
61 if message:
61 if message:
62 message = message.replace('\n\t', ' ')
62 message = message.replace('\n\t', ' ')
63 ui.debug('Subject: %s\n' % message)
63 ui.debug('Subject: %s\n' % message)
64 if user:
64 if user:
65 ui.debug('From: %s\n' % user)
65 ui.debug('From: %s\n' % user)
66 diffs_seen = 0
66 diffs_seen = 0
67 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
67 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
68
68
69 for part in msg.walk():
69 for part in msg.walk():
70 content_type = part.get_content_type()
70 content_type = part.get_content_type()
71 ui.debug('Content-Type: %s\n' % content_type)
71 ui.debug('Content-Type: %s\n' % content_type)
72 if content_type not in ok_types:
72 if content_type not in ok_types:
73 continue
73 continue
74 payload = part.get_payload(decode=True)
74 payload = part.get_payload(decode=True)
75 m = diffre.search(payload)
75 m = diffre.search(payload)
76 if m:
76 if m:
77 ui.debug(_('found patch at byte %d\n') % m.start(0))
77 ui.debug(_('found patch at byte %d\n') % m.start(0))
78 diffs_seen += 1
78 diffs_seen += 1
79 cfp = cStringIO.StringIO()
79 cfp = cStringIO.StringIO()
80 if message:
80 if message:
81 cfp.write(message)
81 cfp.write(message)
82 cfp.write('\n')
82 cfp.write('\n')
83 for line in payload[:m.start(0)].splitlines():
83 for line in payload[:m.start(0)].splitlines():
84 if line.startswith('# HG changeset patch'):
84 if line.startswith('# HG changeset patch'):
85 ui.debug(_('patch generated by hg export\n'))
85 ui.debug(_('patch generated by hg export\n'))
86 hgpatch = True
86 hgpatch = True
87 # drop earlier commit message content
87 # drop earlier commit message content
88 cfp.seek(0)
88 cfp.seek(0)
89 cfp.truncate()
89 cfp.truncate()
90 elif hgpatch:
90 elif hgpatch:
91 if line.startswith('# User '):
91 if line.startswith('# User '):
92 user = line[7:]
92 user = line[7:]
93 ui.debug('From: %s\n' % user)
93 ui.debug('From: %s\n' % user)
94 elif line.startswith("# Date "):
94 elif line.startswith("# Date "):
95 date = line[7:]
95 date = line[7:]
96 if not line.startswith('# '):
96 if not line.startswith('# '):
97 cfp.write(line)
97 cfp.write(line)
98 cfp.write('\n')
98 cfp.write('\n')
99 message = cfp.getvalue()
99 message = cfp.getvalue()
100 if tmpfp:
100 if tmpfp:
101 tmpfp.write(payload)
101 tmpfp.write(payload)
102 if not payload.endswith('\n'):
102 if not payload.endswith('\n'):
103 tmpfp.write('\n')
103 tmpfp.write('\n')
104 elif not diffs_seen and message and content_type == 'text/plain':
104 elif not diffs_seen and message and content_type == 'text/plain':
105 message += '\n' + payload
105 message += '\n' + payload
106 except:
106 except:
107 tmpfp.close()
107 tmpfp.close()
108 os.unlink(tmpname)
108 os.unlink(tmpname)
109 raise
109 raise
110
110
111 tmpfp.close()
111 tmpfp.close()
112 if not diffs_seen:
112 if not diffs_seen:
113 os.unlink(tmpname)
113 os.unlink(tmpname)
114 return None, message, user, date
114 return None, message, user, date
115 return tmpname, message, user, date
115 return tmpname, message, user, date
116
116
117 GP_PATCH = 1 << 0 # we have to run patch
117 GP_PATCH = 1 << 0 # we have to run patch
118 GP_FILTER = 1 << 1 # there's some copy/rename operation
118 GP_FILTER = 1 << 1 # there's some copy/rename operation
119 GP_BINARY = 1 << 2 # there's a binary patch
119 GP_BINARY = 1 << 2 # there's a binary patch
120
120
121 def readgitpatch(patchname):
121 def readgitpatch(patchname):
122 """extract git-style metadata about patches from <patchname>"""
122 """extract git-style metadata about patches from <patchname>"""
123 class gitpatch:
123 class gitpatch:
124 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
124 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
125 def __init__(self, path):
125 def __init__(self, path):
126 self.path = path
126 self.path = path
127 self.oldpath = None
127 self.oldpath = None
128 self.mode = None
128 self.mode = None
129 self.op = 'MODIFY'
129 self.op = 'MODIFY'
130 self.copymod = False
130 self.copymod = False
131 self.lineno = 0
131 self.lineno = 0
132 self.binary = False
132 self.binary = False
133
133
134 # Filter patch for git information
134 # Filter patch for git information
135 gitre = re.compile('diff --git a/(.*) b/(.*)')
135 gitre = re.compile('diff --git a/(.*) b/(.*)')
136 pf = file(patchname)
136 pf = file(patchname)
137 gp = None
137 gp = None
138 gitpatches = []
138 gitpatches = []
139 # Can have a git patch with only metadata, causing patch to complain
139 # Can have a git patch with only metadata, causing patch to complain
140 dopatch = 0
140 dopatch = 0
141
141
142 lineno = 0
142 lineno = 0
143 for line in pf:
143 for line in pf:
144 lineno += 1
144 lineno += 1
145 if line.startswith('diff --git'):
145 if line.startswith('diff --git'):
146 m = gitre.match(line)
146 m = gitre.match(line)
147 if m:
147 if m:
148 if gp:
148 if gp:
149 gitpatches.append(gp)
149 gitpatches.append(gp)
150 src, dst = m.group(1, 2)
150 src, dst = m.group(1, 2)
151 gp = gitpatch(dst)
151 gp = gitpatch(dst)
152 gp.lineno = lineno
152 gp.lineno = lineno
153 elif gp:
153 elif gp:
154 if line.startswith('--- '):
154 if line.startswith('--- '):
155 if gp.op in ('COPY', 'RENAME'):
155 if gp.op in ('COPY', 'RENAME'):
156 gp.copymod = True
156 gp.copymod = True
157 dopatch |= GP_FILTER
157 dopatch |= GP_FILTER
158 gitpatches.append(gp)
158 gitpatches.append(gp)
159 gp = None
159 gp = None
160 dopatch |= GP_PATCH
160 dopatch |= GP_PATCH
161 continue
161 continue
162 if line.startswith('rename from '):
162 if line.startswith('rename from '):
163 gp.op = 'RENAME'
163 gp.op = 'RENAME'
164 gp.oldpath = line[12:].rstrip()
164 gp.oldpath = line[12:].rstrip()
165 elif line.startswith('rename to '):
165 elif line.startswith('rename to '):
166 gp.path = line[10:].rstrip()
166 gp.path = line[10:].rstrip()
167 elif line.startswith('copy from '):
167 elif line.startswith('copy from '):
168 gp.op = 'COPY'
168 gp.op = 'COPY'
169 gp.oldpath = line[10:].rstrip()
169 gp.oldpath = line[10:].rstrip()
170 elif line.startswith('copy to '):
170 elif line.startswith('copy to '):
171 gp.path = line[8:].rstrip()
171 gp.path = line[8:].rstrip()
172 elif line.startswith('deleted file'):
172 elif line.startswith('deleted file'):
173 gp.op = 'DELETE'
173 gp.op = 'DELETE'
174 elif line.startswith('new file mode '):
174 elif line.startswith('new file mode '):
175 gp.op = 'ADD'
175 gp.op = 'ADD'
176 gp.mode = int(line.rstrip()[-3:], 8)
176 gp.mode = int(line.rstrip()[-3:], 8)
177 elif line.startswith('new mode '):
177 elif line.startswith('new mode '):
178 gp.mode = int(line.rstrip()[-3:], 8)
178 gp.mode = int(line.rstrip()[-3:], 8)
179 elif line.startswith('GIT binary patch'):
179 elif line.startswith('GIT binary patch'):
180 dopatch |= GP_BINARY
180 dopatch |= GP_BINARY
181 gp.binary = True
181 gp.binary = True
182 if gp:
182 if gp:
183 gitpatches.append(gp)
183 gitpatches.append(gp)
184
184
185 if not gitpatches:
185 if not gitpatches:
186 dopatch = GP_PATCH
186 dopatch = GP_PATCH
187
187
188 return (dopatch, gitpatches)
188 return (dopatch, gitpatches)
189
189
190 def dogitpatch(patchname, gitpatches, cwd=None):
190 def dogitpatch(patchname, gitpatches, cwd=None):
191 """Preprocess git patch so that vanilla patch can handle it"""
191 """Preprocess git patch so that vanilla patch can handle it"""
192 def extractbin(fp):
192 def extractbin(fp):
193 i = [0] # yuck
193 i = [0] # yuck
194 def readline():
194 def readline():
195 i[0] += 1
195 i[0] += 1
196 return fp.readline().rstrip()
196 return fp.readline().rstrip()
197 line = readline()
197 line = readline()
198 while line and not line.startswith('literal '):
198 while line and not line.startswith('literal '):
199 line = readline()
199 line = readline()
200 if not line:
200 if not line:
201 return None, i[0]
201 return None, i[0]
202 size = int(line[8:])
202 size = int(line[8:])
203 dec = []
203 dec = []
204 line = readline()
204 line = readline()
205 while line:
205 while line:
206 l = line[0]
206 l = line[0]
207 if l <= 'Z' and l >= 'A':
207 if l <= 'Z' and l >= 'A':
208 l = ord(l) - ord('A') + 1
208 l = ord(l) - ord('A') + 1
209 else:
209 else:
210 l = ord(l) - ord('a') + 27
210 l = ord(l) - ord('a') + 27
211 dec.append(base85.b85decode(line[1:])[:l])
211 dec.append(base85.b85decode(line[1:])[:l])
212 line = readline()
212 line = readline()
213 text = zlib.decompress(''.join(dec))
213 text = zlib.decompress(''.join(dec))
214 if len(text) != size:
214 if len(text) != size:
215 raise util.Abort(_('binary patch is %d bytes, not %d') %
215 raise util.Abort(_('binary patch is %d bytes, not %d') %
216 (len(text), size))
216 (len(text), size))
217 return text, i[0]
217 return text, i[0]
218
218
219 pf = file(patchname)
219 pf = file(patchname)
220 pfline = 1
220 pfline = 1
221
221
222 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
222 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
223 tmpfp = os.fdopen(fd, 'w')
223 tmpfp = os.fdopen(fd, 'w')
224
224
225 try:
225 try:
226 for i in xrange(len(gitpatches)):
226 for i in xrange(len(gitpatches)):
227 p = gitpatches[i]
227 p = gitpatches[i]
228 if not p.copymod and not p.binary:
228 if not p.copymod and not p.binary:
229 continue
229 continue
230
230
231 # rewrite patch hunk
231 # rewrite patch hunk
232 while pfline < p.lineno:
232 while pfline < p.lineno:
233 tmpfp.write(pf.readline())
233 tmpfp.write(pf.readline())
234 pfline += 1
234 pfline += 1
235
235
236 if p.binary:
236 if p.binary:
237 text, delta = extractbin(pf)
237 text, delta = extractbin(pf)
238 if not text:
238 if not text:
239 raise util.Abort(_('binary patch extraction failed'))
239 raise util.Abort(_('binary patch extraction failed'))
240 pfline += delta
240 pfline += delta
241 if not cwd:
241 if not cwd:
242 cwd = os.getcwd()
242 cwd = os.getcwd()
243 absdst = os.path.join(cwd, p.path)
243 absdst = os.path.join(cwd, p.path)
244 basedir = os.path.dirname(absdst)
244 basedir = os.path.dirname(absdst)
245 if not os.path.isdir(basedir):
245 if not os.path.isdir(basedir):
246 os.makedirs(basedir)
246 os.makedirs(basedir)
247 out = file(absdst, 'wb')
247 out = file(absdst, 'wb')
248 out.write(text)
248 out.write(text)
249 out.close()
249 out.close()
250 elif p.copymod:
250 elif p.copymod:
251 copyfile(p.oldpath, p.path, basedir=cwd)
251 copyfile(p.oldpath, p.path, basedir=cwd)
252 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
252 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
253 line = pf.readline()
253 line = pf.readline()
254 pfline += 1
254 pfline += 1
255 while not line.startswith('--- a/'):
255 while not line.startswith('--- a/'):
256 tmpfp.write(line)
256 tmpfp.write(line)
257 line = pf.readline()
257 line = pf.readline()
258 pfline += 1
258 pfline += 1
259 tmpfp.write('--- a/%s\n' % p.path)
259 tmpfp.write('--- a/%s\n' % p.path)
260
260
261 line = pf.readline()
261 line = pf.readline()
262 while line:
262 while line:
263 tmpfp.write(line)
263 tmpfp.write(line)
264 line = pf.readline()
264 line = pf.readline()
265 except:
265 except:
266 tmpfp.close()
266 tmpfp.close()
267 os.unlink(patchname)
267 os.unlink(patchname)
268 raise
268 raise
269
269
270 tmpfp.close()
270 tmpfp.close()
271 return patchname
271 return patchname
272
272
273 def patch(patchname, ui, strip=1, cwd=None, files={}):
273 def patch(patchname, ui, strip=1, cwd=None, files={}):
274 """apply the patch <patchname> to the working directory.
274 """apply the patch <patchname> to the working directory.
275 a list of patched files is returned"""
275 a list of patched files is returned"""
276
276
277 # helper function
277 # helper function
278 def __patch(patchname):
278 def __patch(patchname):
279 """patch and updates the files and fuzz variables"""
279 """patch and updates the files and fuzz variables"""
280 fuzz = False
280 fuzz = False
281
281
282 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''),
282 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''),
283 'patch')
283 'patch')
284 args = []
284 args = []
285 if cwd:
285 if cwd:
286 args.append('-d %s' % util.shellquote(cwd))
286 args.append('-d %s' % util.shellquote(cwd))
287 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
287 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
288 util.shellquote(patchname)))
288 util.shellquote(patchname)))
289
289
290 for line in fp:
290 for line in fp:
291 line = line.rstrip()
291 line = line.rstrip()
292 ui.note(line + '\n')
292 ui.note(line + '\n')
293 if line.startswith('patching file '):
293 if line.startswith('patching file '):
294 pf = util.parse_patch_output(line)
294 pf = util.parse_patch_output(line)
295 printed_file = False
295 printed_file = False
296 files.setdefault(pf, (None, None))
296 files.setdefault(pf, (None, None))
297 elif line.find('with fuzz') >= 0:
297 elif line.find('with fuzz') >= 0:
298 fuzz = True
298 fuzz = True
299 if not printed_file:
299 if not printed_file:
300 ui.warn(pf + '\n')
300 ui.warn(pf + '\n')
301 printed_file = True
301 printed_file = True
302 ui.warn(line + '\n')
302 ui.warn(line + '\n')
303 elif line.find('saving rejects to file') >= 0:
303 elif line.find('saving rejects to file') >= 0:
304 ui.warn(line + '\n')
304 ui.warn(line + '\n')
305 elif line.find('FAILED') >= 0:
305 elif line.find('FAILED') >= 0:
306 if not printed_file:
306 if not printed_file:
307 ui.warn(pf + '\n')
307 ui.warn(pf + '\n')
308 printed_file = True
308 printed_file = True
309 ui.warn(line + '\n')
309 ui.warn(line + '\n')
310 code = fp.close()
310 code = fp.close()
311 if code:
311 if code:
312 raise util.Abort(_("patch command failed: %s") %
312 raise util.Abort(_("patch command failed: %s") %
313 util.explain_exit(code)[0])
313 util.explain_exit(code)[0])
314 return fuzz
314 return fuzz
315
315
316 (dopatch, gitpatches) = readgitpatch(patchname)
316 (dopatch, gitpatches) = readgitpatch(patchname)
317 for gp in gitpatches:
317 for gp in gitpatches:
318 files[gp.path] = (gp.op, gp)
318 files[gp.path] = (gp.op, gp)
319
319
320 fuzz = False
320 fuzz = False
321 if dopatch:
321 if dopatch:
322 filterpatch = dopatch & (GP_FILTER | GP_BINARY)
322 filterpatch = dopatch & (GP_FILTER | GP_BINARY)
323 if filterpatch:
323 if filterpatch:
324 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
324 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
325 try:
325 try:
326 if dopatch & GP_PATCH:
326 if dopatch & GP_PATCH:
327 fuzz = __patch(patchname)
327 fuzz = __patch(patchname)
328 finally:
328 finally:
329 if filterpatch:
329 if filterpatch:
330 os.unlink(patchname)
330 os.unlink(patchname)
331
331
332 return fuzz
332 return fuzz
333
333
334 def diffopts(ui, opts={}, untrusted=False):
334 def diffopts(ui, opts={}, untrusted=False):
335 def get(key, name=None):
335 def get(key, name=None):
336 return (opts.get(key) or
336 return (opts.get(key) or
337 ui.configbool('diff', name or key, None, untrusted=untrusted))
337 ui.configbool('diff', name or key, None, untrusted=untrusted))
338 return mdiff.diffopts(
338 return mdiff.diffopts(
339 text=opts.get('text'),
339 text=opts.get('text'),
340 git=get('git'),
340 git=get('git'),
341 nodates=get('nodates'),
341 nodates=get('nodates'),
342 showfunc=get('show_function', 'showfunc'),
342 showfunc=get('show_function', 'showfunc'),
343 ignorews=get('ignore_all_space', 'ignorews'),
343 ignorews=get('ignore_all_space', 'ignorews'),
344 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
344 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
345 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
345 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
346
346
347 def updatedir(ui, repo, patches, wlock=None):
347 def updatedir(ui, repo, patches, wlock=None):
348 '''Update dirstate after patch application according to metadata'''
348 '''Update dirstate after patch application according to metadata'''
349 if not patches:
349 if not patches:
350 return
350 return
351 copies = []
351 copies = []
352 removes = {}
352 removes = {}
353 cfiles = patches.keys()
353 cfiles = patches.keys()
354 cwd = repo.getcwd()
354 cwd = repo.getcwd()
355 if cwd:
355 if cwd:
356 cfiles = [util.pathto(cwd, f) for f in patches.keys()]
356 cfiles = [util.pathto(cwd, f) for f in patches.keys()]
357 for f in patches:
357 for f in patches:
358 ctype, gp = patches[f]
358 ctype, gp = patches[f]
359 if ctype == 'RENAME':
359 if ctype == 'RENAME':
360 copies.append((gp.oldpath, gp.path, gp.copymod))
360 copies.append((gp.oldpath, gp.path, gp.copymod))
361 removes[gp.oldpath] = 1
361 removes[gp.oldpath] = 1
362 elif ctype == 'COPY':
362 elif ctype == 'COPY':
363 copies.append((gp.oldpath, gp.path, gp.copymod))
363 copies.append((gp.oldpath, gp.path, gp.copymod))
364 elif ctype == 'DELETE':
364 elif ctype == 'DELETE':
365 removes[gp.path] = 1
365 removes[gp.path] = 1
366 for src, dst, after in copies:
366 for src, dst, after in copies:
367 if not after:
367 if not after:
368 copyfile(src, dst, repo.root)
368 copyfile(src, dst, repo.root)
369 repo.copy(src, dst, wlock=wlock)
369 repo.copy(src, dst, wlock=wlock)
370 removes = removes.keys()
370 removes = removes.keys()
371 if removes:
371 if removes:
372 removes.sort()
372 removes.sort()
373 repo.remove(removes, True, wlock=wlock)
373 repo.remove(removes, True, wlock=wlock)
374 for f in patches:
374 for f in patches:
375 ctype, gp = patches[f]
375 ctype, gp = patches[f]
376 if gp and gp.mode:
376 if gp and gp.mode:
377 x = gp.mode & 0100 != 0
377 x = gp.mode & 0100 != 0
378 dst = os.path.join(repo.root, gp.path)
378 dst = os.path.join(repo.root, gp.path)
379 # patch won't create empty files
379 # patch won't create empty files
380 if ctype == 'ADD' and not os.path.exists(dst):
380 if ctype == 'ADD' and not os.path.exists(dst):
381 repo.wwrite(gp.path, '')
381 repo.wwrite(gp.path, '')
382 util.set_exec(dst, x)
382 util.set_exec(dst, x)
383 cmdutil.addremove(repo, cfiles, wlock=wlock)
383 cmdutil.addremove(repo, cfiles, wlock=wlock)
384 files = patches.keys()
384 files = patches.keys()
385 files.extend([r for r in removes if r not in files])
385 files.extend([r for r in removes if r not in files])
386 files.sort()
386 files.sort()
387
387
388 return files
388 return files
389
389
390 def b85diff(fp, to, tn):
390 def b85diff(fp, to, tn):
391 '''print base85-encoded binary diff'''
391 '''print base85-encoded binary diff'''
392 def gitindex(text):
392 def gitindex(text):
393 if not text:
393 if not text:
394 return '0' * 40
394 return '0' * 40
395 l = len(text)
395 l = len(text)
396 s = sha.new('blob %d\0' % l)
396 s = sha.new('blob %d\0' % l)
397 s.update(text)
397 s.update(text)
398 return s.hexdigest()
398 return s.hexdigest()
399
399
400 def fmtline(line):
400 def fmtline(line):
401 l = len(line)
401 l = len(line)
402 if l <= 26:
402 if l <= 26:
403 l = chr(ord('A') + l - 1)
403 l = chr(ord('A') + l - 1)
404 else:
404 else:
405 l = chr(l - 26 + ord('a') - 1)
405 l = chr(l - 26 + ord('a') - 1)
406 return '%c%s\n' % (l, base85.b85encode(line, True))
406 return '%c%s\n' % (l, base85.b85encode(line, True))
407
407
408 def chunk(text, csize=52):
408 def chunk(text, csize=52):
409 l = len(text)
409 l = len(text)
410 i = 0
410 i = 0
411 while i < l:
411 while i < l:
412 yield text[i:i+csize]
412 yield text[i:i+csize]
413 i += csize
413 i += csize
414
414
415 if to == tn:
415 tohash = gitindex(to)
416 tnhash = gitindex(tn)
417 if tohash == tnhash:
416 return
418 return
417 # TODO: deltas
419 # TODO: deltas
418 l = len(tn)
419 fp.write('index %s..%s\nGIT binary patch\nliteral %s\n' %
420 fp.write('index %s..%s\nGIT binary patch\nliteral %s\n' %
420 (gitindex(to), gitindex(tn), len(tn)))
421 (tohash, tnhash, len(tn)))
421
422
422 tn = ''.join([fmtline(l) for l in chunk(zlib.compress(tn))])
423 tn = ''.join([fmtline(l) for l in chunk(zlib.compress(tn))])
423 fp.write(tn)
424 fp.write(tn)
424 fp.write('\n')
425 fp.write('\n')
425
426
426 def diff(repo, node1=None, node2=None, files=None, match=util.always,
427 def diff(repo, node1=None, node2=None, files=None, match=util.always,
427 fp=None, changes=None, opts=None):
428 fp=None, changes=None, opts=None):
428 '''print diff of changes to files between two nodes, or node and
429 '''print diff of changes to files between two nodes, or node and
429 working directory.
430 working directory.
430
431
431 if node1 is None, use first dirstate parent instead.
432 if node1 is None, use first dirstate parent instead.
432 if node2 is None, compare node1 with working directory.'''
433 if node2 is None, compare node1 with working directory.'''
433
434
434 if opts is None:
435 if opts is None:
435 opts = mdiff.defaultopts
436 opts = mdiff.defaultopts
436 if fp is None:
437 if fp is None:
437 fp = repo.ui
438 fp = repo.ui
438
439
439 if not node1:
440 if not node1:
440 node1 = repo.dirstate.parents()[0]
441 node1 = repo.dirstate.parents()[0]
441
442
442 clcache = {}
443 clcache = {}
443 def getchangelog(n):
444 def getchangelog(n):
444 if n not in clcache:
445 if n not in clcache:
445 clcache[n] = repo.changelog.read(n)
446 clcache[n] = repo.changelog.read(n)
446 return clcache[n]
447 return clcache[n]
447 mcache = {}
448 mcache = {}
448 def getmanifest(n):
449 def getmanifest(n):
449 if n not in mcache:
450 if n not in mcache:
450 mcache[n] = repo.manifest.read(n)
451 mcache[n] = repo.manifest.read(n)
451 return mcache[n]
452 return mcache[n]
452 fcache = {}
453 fcache = {}
453 def getfile(f):
454 def getfile(f):
454 if f not in fcache:
455 if f not in fcache:
455 fcache[f] = repo.file(f)
456 fcache[f] = repo.file(f)
456 return fcache[f]
457 return fcache[f]
457
458
458 # reading the data for node1 early allows it to play nicely
459 # reading the data for node1 early allows it to play nicely
459 # with repo.status and the revlog cache.
460 # with repo.status and the revlog cache.
460 change = getchangelog(node1)
461 change = getchangelog(node1)
461 mmap = getmanifest(change[0])
462 mmap = getmanifest(change[0])
462 date1 = util.datestr(change[2])
463 date1 = util.datestr(change[2])
463
464
464 if not changes:
465 if not changes:
465 changes = repo.status(node1, node2, files, match=match)[:5]
466 changes = repo.status(node1, node2, files, match=match)[:5]
466 modified, added, removed, deleted, unknown = changes
467 modified, added, removed, deleted, unknown = changes
467 if files:
468 if files:
468 def filterfiles(filters):
469 def filterfiles(filters):
469 l = [x for x in filters if x in files]
470 l = [x for x in filters if x in files]
470
471
471 for t in files:
472 for t in files:
472 if not t.endswith("/"):
473 if not t.endswith("/"):
473 t += "/"
474 t += "/"
474 l += [x for x in filters if x.startswith(t)]
475 l += [x for x in filters if x.startswith(t)]
475 return l
476 return l
476
477
477 modified, added, removed = map(filterfiles, (modified, added, removed))
478 modified, added, removed = map(filterfiles, (modified, added, removed))
478
479
479 if not modified and not added and not removed:
480 if not modified and not added and not removed:
480 return
481 return
481
482
482 # returns False if there was no rename between n1 and n2
483 # returns False if there was no rename between n1 and n2
483 # returns None if the file was created between n1 and n2
484 # returns None if the file was created between n1 and n2
484 # returns the (file, node) present in n1 that was renamed to f in n2
485 # returns the (file, node) present in n1 that was renamed to f in n2
485 def renamedbetween(f, n1, n2):
486 def renamedbetween(f, n1, n2):
486 r1, r2 = map(repo.changelog.rev, (n1, n2))
487 r1, r2 = map(repo.changelog.rev, (n1, n2))
487 orig = f
488 orig = f
488 src = None
489 src = None
489 while r2 > r1:
490 while r2 > r1:
490 cl = getchangelog(n2)
491 cl = getchangelog(n2)
491 if f in cl[3]:
492 if f in cl[3]:
492 m = getmanifest(cl[0])
493 m = getmanifest(cl[0])
493 try:
494 try:
494 src = getfile(f).renamed(m[f])
495 src = getfile(f).renamed(m[f])
495 except KeyError:
496 except KeyError:
496 return None
497 return None
497 if src:
498 if src:
498 f = src[0]
499 f = src[0]
499 n2 = repo.changelog.parents(n2)[0]
500 n2 = repo.changelog.parents(n2)[0]
500 r2 = repo.changelog.rev(n2)
501 r2 = repo.changelog.rev(n2)
501 cl = getchangelog(n1)
502 cl = getchangelog(n1)
502 m = getmanifest(cl[0])
503 m = getmanifest(cl[0])
503 if f not in m:
504 if f not in m:
504 return None
505 return None
505 if f == orig:
506 if f == orig:
506 return False
507 return False
507 return f, m[f]
508 return f, m[f]
508
509
509 if node2:
510 if node2:
510 change = getchangelog(node2)
511 change = getchangelog(node2)
511 mmap2 = getmanifest(change[0])
512 mmap2 = getmanifest(change[0])
512 _date2 = util.datestr(change[2])
513 _date2 = util.datestr(change[2])
513 def date2(f):
514 def date2(f):
514 return _date2
515 return _date2
515 def read(f):
516 def read(f):
516 return getfile(f).read(mmap2[f])
517 return getfile(f).read(mmap2[f])
517 def renamed(f):
518 def renamed(f):
518 return renamedbetween(f, node1, node2)
519 return renamedbetween(f, node1, node2)
519 else:
520 else:
520 tz = util.makedate()[1]
521 tz = util.makedate()[1]
521 _date2 = util.datestr()
522 _date2 = util.datestr()
522 def date2(f):
523 def date2(f):
523 try:
524 try:
524 return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz))
525 return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz))
525 except OSError, err:
526 except OSError, err:
526 if err.errno != errno.ENOENT: raise
527 if err.errno != errno.ENOENT: raise
527 return _date2
528 return _date2
528 def read(f):
529 def read(f):
529 return repo.wread(f)
530 return repo.wread(f)
530 def renamed(f):
531 def renamed(f):
531 src = repo.dirstate.copied(f)
532 src = repo.dirstate.copied(f)
532 parent = repo.dirstate.parents()[0]
533 parent = repo.dirstate.parents()[0]
533 if src:
534 if src:
534 f = src
535 f = src
535 of = renamedbetween(f, node1, parent)
536 of = renamedbetween(f, node1, parent)
536 if of or of is None:
537 if of or of is None:
537 return of
538 return of
538 elif src:
539 elif src:
539 cl = getchangelog(parent)[0]
540 cl = getchangelog(parent)[0]
540 return (src, getmanifest(cl)[src])
541 return (src, getmanifest(cl)[src])
541 else:
542 else:
542 return None
543 return None
543
544
544 if repo.ui.quiet:
545 if repo.ui.quiet:
545 r = None
546 r = None
546 else:
547 else:
547 hexfunc = repo.ui.debugflag and hex or short
548 hexfunc = repo.ui.debugflag and hex or short
548 r = [hexfunc(node) for node in [node1, node2] if node]
549 r = [hexfunc(node) for node in [node1, node2] if node]
549
550
550 if opts.git:
551 if opts.git:
551 copied = {}
552 copied = {}
552 for f in added:
553 for f in added:
553 src = renamed(f)
554 src = renamed(f)
554 if src:
555 if src:
555 copied[f] = src
556 copied[f] = src
556 srcs = [x[1][0] for x in copied.items()]
557 srcs = [x[1][0] for x in copied.items()]
557
558
558 all = modified + added + removed
559 all = modified + added + removed
559 all.sort()
560 all.sort()
560 gone = {}
561 gone = {}
561 for f in all:
562 for f in all:
562 to = None
563 to = None
563 tn = None
564 tn = None
564 dodiff = True
565 dodiff = True
565 header = []
566 header = []
566 if f in mmap:
567 if f in mmap:
567 to = getfile(f).read(mmap[f])
568 to = getfile(f).read(mmap[f])
568 if f not in removed:
569 if f not in removed:
569 tn = read(f)
570 tn = read(f)
570 if opts.git:
571 if opts.git:
571 def gitmode(x):
572 def gitmode(x):
572 return x and '100755' or '100644'
573 return x and '100755' or '100644'
573 def addmodehdr(header, omode, nmode):
574 def addmodehdr(header, omode, nmode):
574 if omode != nmode:
575 if omode != nmode:
575 header.append('old mode %s\n' % omode)
576 header.append('old mode %s\n' % omode)
576 header.append('new mode %s\n' % nmode)
577 header.append('new mode %s\n' % nmode)
577
578
578 a, b = f, f
579 a, b = f, f
579 if f in added:
580 if f in added:
580 if node2:
581 if node2:
581 mode = gitmode(mmap2.execf(f))
582 mode = gitmode(mmap2.execf(f))
582 else:
583 else:
583 mode = gitmode(util.is_exec(repo.wjoin(f), None))
584 mode = gitmode(util.is_exec(repo.wjoin(f), None))
584 if f in copied:
585 if f in copied:
585 a, arev = copied[f]
586 a, arev = copied[f]
586 omode = gitmode(mmap.execf(a))
587 omode = gitmode(mmap.execf(a))
587 addmodehdr(header, omode, mode)
588 addmodehdr(header, omode, mode)
588 if a in removed and a not in gone:
589 if a in removed and a not in gone:
589 op = 'rename'
590 op = 'rename'
590 gone[a] = 1
591 gone[a] = 1
591 else:
592 else:
592 op = 'copy'
593 op = 'copy'
593 header.append('%s from %s\n' % (op, a))
594 header.append('%s from %s\n' % (op, a))
594 header.append('%s to %s\n' % (op, f))
595 header.append('%s to %s\n' % (op, f))
595 to = getfile(a).read(arev)
596 to = getfile(a).read(arev)
596 else:
597 else:
597 header.append('new file mode %s\n' % mode)
598 header.append('new file mode %s\n' % mode)
598 if util.binary(tn):
599 if util.binary(tn):
599 dodiff = 'binary'
600 dodiff = 'binary'
600 elif f in removed:
601 elif f in removed:
601 if f in srcs:
602 if f in srcs:
602 dodiff = False
603 dodiff = False
603 else:
604 else:
604 mode = gitmode(mmap.execf(f))
605 mode = gitmode(mmap.execf(f))
605 header.append('deleted file mode %s\n' % mode)
606 header.append('deleted file mode %s\n' % mode)
606 else:
607 else:
607 omode = gitmode(mmap.execf(f))
608 omode = gitmode(mmap.execf(f))
608 if node2:
609 if node2:
609 nmode = gitmode(mmap2.execf(f))
610 nmode = gitmode(mmap2.execf(f))
610 else:
611 else:
611 nmode = gitmode(util.is_exec(repo.wjoin(f), mmap.execf(f)))
612 nmode = gitmode(util.is_exec(repo.wjoin(f), mmap.execf(f)))
612 addmodehdr(header, omode, nmode)
613 addmodehdr(header, omode, nmode)
613 if util.binary(to) or util.binary(tn):
614 if util.binary(to) or util.binary(tn):
614 dodiff = 'binary'
615 dodiff = 'binary'
615 r = None
616 r = None
616 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
617 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
617 if dodiff == 'binary':
618 if dodiff == 'binary':
618 fp.write(''.join(header))
619 fp.write(''.join(header))
619 b85diff(fp, to, tn)
620 b85diff(fp, to, tn)
620 elif dodiff:
621 elif dodiff:
621 text = mdiff.unidiff(to, date1, tn, date2(f), f, r, opts=opts)
622 text = mdiff.unidiff(to, date1, tn, date2(f), f, r, opts=opts)
622 if text or len(header) > 1:
623 if text or len(header) > 1:
623 fp.write(''.join(header))
624 fp.write(''.join(header))
624 fp.write(text)
625 fp.write(text)
625
626
626 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
627 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
627 opts=None):
628 opts=None):
628 '''export changesets as hg patches.'''
629 '''export changesets as hg patches.'''
629
630
630 total = len(revs)
631 total = len(revs)
631 revwidth = max([len(str(rev)) for rev in revs])
632 revwidth = max([len(str(rev)) for rev in revs])
632
633
633 def single(node, seqno, fp):
634 def single(node, seqno, fp):
634 parents = [p for p in repo.changelog.parents(node) if p != nullid]
635 parents = [p for p in repo.changelog.parents(node) if p != nullid]
635 if switch_parent:
636 if switch_parent:
636 parents.reverse()
637 parents.reverse()
637 prev = (parents and parents[0]) or nullid
638 prev = (parents and parents[0]) or nullid
638 change = repo.changelog.read(node)
639 change = repo.changelog.read(node)
639
640
640 if not fp:
641 if not fp:
641 fp = cmdutil.make_file(repo, template, node, total=total,
642 fp = cmdutil.make_file(repo, template, node, total=total,
642 seqno=seqno, revwidth=revwidth)
643 seqno=seqno, revwidth=revwidth)
643 if fp not in (sys.stdout, repo.ui):
644 if fp not in (sys.stdout, repo.ui):
644 repo.ui.note("%s\n" % fp.name)
645 repo.ui.note("%s\n" % fp.name)
645
646
646 fp.write("# HG changeset patch\n")
647 fp.write("# HG changeset patch\n")
647 fp.write("# User %s\n" % change[1])
648 fp.write("# User %s\n" % change[1])
648 fp.write("# Date %d %d\n" % change[2])
649 fp.write("# Date %d %d\n" % change[2])
649 fp.write("# Node ID %s\n" % hex(node))
650 fp.write("# Node ID %s\n" % hex(node))
650 fp.write("# Parent %s\n" % hex(prev))
651 fp.write("# Parent %s\n" % hex(prev))
651 if len(parents) > 1:
652 if len(parents) > 1:
652 fp.write("# Parent %s\n" % hex(parents[1]))
653 fp.write("# Parent %s\n" % hex(parents[1]))
653 fp.write(change[4].rstrip())
654 fp.write(change[4].rstrip())
654 fp.write("\n\n")
655 fp.write("\n\n")
655
656
656 diff(repo, prev, node, fp=fp, opts=opts)
657 diff(repo, prev, node, fp=fp, opts=opts)
657 if fp not in (sys.stdout, repo.ui):
658 if fp not in (sys.stdout, repo.ui):
658 fp.close()
659 fp.close()
659
660
660 for seqno, rev in enumerate(revs):
661 for seqno, rev in enumerate(revs):
661 single(repo.lookup(rev), seqno+1, fp)
662 single(repo.lookup(rev), seqno+1, fp)
662
663
663 def diffstat(patchlines):
664 def diffstat(patchlines):
664 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
665 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
665 try:
666 try:
666 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
667 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
667 try:
668 try:
668 for line in patchlines: print >> p.tochild, line
669 for line in patchlines: print >> p.tochild, line
669 p.tochild.close()
670 p.tochild.close()
670 if p.wait(): return
671 if p.wait(): return
671 fp = os.fdopen(fd, 'r')
672 fp = os.fdopen(fd, 'r')
672 stat = []
673 stat = []
673 for line in fp: stat.append(line.lstrip())
674 for line in fp: stat.append(line.lstrip())
674 last = stat.pop()
675 last = stat.pop()
675 stat.insert(0, last)
676 stat.insert(0, last)
676 stat = ''.join(stat)
677 stat = ''.join(stat)
677 if stat.startswith('0 files'): raise ValueError
678 if stat.startswith('0 files'): raise ValueError
678 return stat
679 return stat
679 except: raise
680 except: raise
680 finally:
681 finally:
681 try: os.unlink(name)
682 try: os.unlink(name)
682 except: pass
683 except: pass
General Comments 0
You need to be logged in to leave comments. Login now