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