##// END OF EJS Templates
Don't generate git diff header for empty diffs
Brendan Cully -
r3329:319358e6 default
parent child Browse files
Show More
@@ -1,575 +1,576
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(), "cmdutil mdiff util")
11 demandload(globals(), "cmdutil mdiff util")
12 demandload(globals(), '''cStringIO email.Parser errno os re shutil sys tempfile
12 demandload(globals(), '''cStringIO email.Parser errno os re shutil sys tempfile
13 popen2''')
13 popen2''')
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
131
132 # Filter patch for git information
132 # Filter patch for git information
133 gitre = re.compile('diff --git a/(.*) b/(.*)')
133 gitre = re.compile('diff --git a/(.*) b/(.*)')
134 pf = file(patchname)
134 pf = file(patchname)
135 gp = None
135 gp = None
136 gitpatches = []
136 gitpatches = []
137 # Can have a git patch with only metadata, causing patch to complain
137 # Can have a git patch with only metadata, causing patch to complain
138 dopatch = False
138 dopatch = False
139
139
140 lineno = 0
140 lineno = 0
141 for line in pf:
141 for line in pf:
142 lineno += 1
142 lineno += 1
143 if line.startswith('diff --git'):
143 if line.startswith('diff --git'):
144 m = gitre.match(line)
144 m = gitre.match(line)
145 if m:
145 if m:
146 if gp:
146 if gp:
147 gitpatches.append(gp)
147 gitpatches.append(gp)
148 src, dst = m.group(1,2)
148 src, dst = m.group(1,2)
149 gp = gitpatch(dst)
149 gp = gitpatch(dst)
150 gp.lineno = lineno
150 gp.lineno = lineno
151 elif gp:
151 elif gp:
152 if line.startswith('--- '):
152 if line.startswith('--- '):
153 if gp.op in ('COPY', 'RENAME'):
153 if gp.op in ('COPY', 'RENAME'):
154 gp.copymod = True
154 gp.copymod = True
155 dopatch = 'filter'
155 dopatch = 'filter'
156 gitpatches.append(gp)
156 gitpatches.append(gp)
157 gp = None
157 gp = None
158 if not dopatch:
158 if not dopatch:
159 dopatch = True
159 dopatch = True
160 continue
160 continue
161 if line.startswith('rename from '):
161 if line.startswith('rename from '):
162 gp.op = 'RENAME'
162 gp.op = 'RENAME'
163 gp.oldpath = line[12:].rstrip()
163 gp.oldpath = line[12:].rstrip()
164 elif line.startswith('rename to '):
164 elif line.startswith('rename to '):
165 gp.path = line[10:].rstrip()
165 gp.path = line[10:].rstrip()
166 elif line.startswith('copy from '):
166 elif line.startswith('copy from '):
167 gp.op = 'COPY'
167 gp.op = 'COPY'
168 gp.oldpath = line[10:].rstrip()
168 gp.oldpath = line[10:].rstrip()
169 elif line.startswith('copy to '):
169 elif line.startswith('copy to '):
170 gp.path = line[8:].rstrip()
170 gp.path = line[8:].rstrip()
171 elif line.startswith('deleted file'):
171 elif line.startswith('deleted file'):
172 gp.op = 'DELETE'
172 gp.op = 'DELETE'
173 elif line.startswith('new file mode '):
173 elif line.startswith('new file mode '):
174 gp.op = 'ADD'
174 gp.op = 'ADD'
175 gp.mode = int(line.rstrip()[-3:], 8)
175 gp.mode = int(line.rstrip()[-3:], 8)
176 elif line.startswith('new mode '):
176 elif line.startswith('new mode '):
177 gp.mode = int(line.rstrip()[-3:], 8)
177 gp.mode = int(line.rstrip()[-3:], 8)
178 if gp:
178 if gp:
179 gitpatches.append(gp)
179 gitpatches.append(gp)
180
180
181 if not gitpatches:
181 if not gitpatches:
182 dopatch = True
182 dopatch = True
183
183
184 return (dopatch, gitpatches)
184 return (dopatch, gitpatches)
185
185
186 def dogitpatch(patchname, gitpatches, cwd=None):
186 def dogitpatch(patchname, gitpatches, cwd=None):
187 """Preprocess git patch so that vanilla patch can handle it"""
187 """Preprocess git patch so that vanilla patch can handle it"""
188 pf = file(patchname)
188 pf = file(patchname)
189 pfline = 1
189 pfline = 1
190
190
191 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
191 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
192 tmpfp = os.fdopen(fd, 'w')
192 tmpfp = os.fdopen(fd, 'w')
193
193
194 try:
194 try:
195 for i in range(len(gitpatches)):
195 for i in range(len(gitpatches)):
196 p = gitpatches[i]
196 p = gitpatches[i]
197 if not p.copymod:
197 if not p.copymod:
198 continue
198 continue
199
199
200 copyfile(p.oldpath, p.path, basedir=cwd)
200 copyfile(p.oldpath, p.path, basedir=cwd)
201
201
202 # rewrite patch hunk
202 # rewrite patch hunk
203 while pfline < p.lineno:
203 while pfline < p.lineno:
204 tmpfp.write(pf.readline())
204 tmpfp.write(pf.readline())
205 pfline += 1
205 pfline += 1
206 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
206 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
207 line = pf.readline()
207 line = pf.readline()
208 pfline += 1
208 pfline += 1
209 while not line.startswith('--- a/'):
209 while not line.startswith('--- a/'):
210 tmpfp.write(line)
210 tmpfp.write(line)
211 line = pf.readline()
211 line = pf.readline()
212 pfline += 1
212 pfline += 1
213 tmpfp.write('--- a/%s\n' % p.path)
213 tmpfp.write('--- a/%s\n' % p.path)
214
214
215 line = pf.readline()
215 line = pf.readline()
216 while line:
216 while line:
217 tmpfp.write(line)
217 tmpfp.write(line)
218 line = pf.readline()
218 line = pf.readline()
219 except:
219 except:
220 tmpfp.close()
220 tmpfp.close()
221 os.unlink(patchname)
221 os.unlink(patchname)
222 raise
222 raise
223
223
224 tmpfp.close()
224 tmpfp.close()
225 return patchname
225 return patchname
226
226
227 def patch(patchname, ui, strip=1, cwd=None):
227 def patch(patchname, ui, strip=1, cwd=None):
228 """apply the patch <patchname> to the working directory.
228 """apply the patch <patchname> to the working directory.
229 a list of patched files is returned"""
229 a list of patched files is returned"""
230
230
231 # helper function
231 # helper function
232 def __patch(patchname):
232 def __patch(patchname):
233 """patch and updates the files and fuzz variables"""
233 """patch and updates the files and fuzz variables"""
234 files = {}
234 files = {}
235 fuzz = False
235 fuzz = False
236
236
237 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''),
237 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''),
238 'patch')
238 'patch')
239 args = []
239 args = []
240 if cwd:
240 if cwd:
241 args.append('-d %s' % util.shellquote(cwd))
241 args.append('-d %s' % util.shellquote(cwd))
242 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
242 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
243 util.shellquote(patchname)))
243 util.shellquote(patchname)))
244
244
245 for line in fp:
245 for line in fp:
246 line = line.rstrip()
246 line = line.rstrip()
247 ui.note(line + '\n')
247 ui.note(line + '\n')
248 if line.startswith('patching file '):
248 if line.startswith('patching file '):
249 pf = util.parse_patch_output(line)
249 pf = util.parse_patch_output(line)
250 printed_file = False
250 printed_file = False
251 files.setdefault(pf, (None, None))
251 files.setdefault(pf, (None, None))
252 elif line.find('with fuzz') >= 0:
252 elif line.find('with fuzz') >= 0:
253 fuzz = True
253 fuzz = True
254 if not printed_file:
254 if not printed_file:
255 ui.warn(pf + '\n')
255 ui.warn(pf + '\n')
256 printed_file = True
256 printed_file = True
257 ui.warn(line + '\n')
257 ui.warn(line + '\n')
258 elif line.find('saving rejects to file') >= 0:
258 elif line.find('saving rejects to file') >= 0:
259 ui.warn(line + '\n')
259 ui.warn(line + '\n')
260 elif line.find('FAILED') >= 0:
260 elif line.find('FAILED') >= 0:
261 if not printed_file:
261 if not printed_file:
262 ui.warn(pf + '\n')
262 ui.warn(pf + '\n')
263 printed_file = True
263 printed_file = True
264 ui.warn(line + '\n')
264 ui.warn(line + '\n')
265 code = fp.close()
265 code = fp.close()
266 if code:
266 if code:
267 raise util.Abort(_("patch command failed: %s") %
267 raise util.Abort(_("patch command failed: %s") %
268 util.explain_exit(code)[0])
268 util.explain_exit(code)[0])
269 return files, fuzz
269 return files, fuzz
270
270
271 (dopatch, gitpatches) = readgitpatch(patchname)
271 (dopatch, gitpatches) = readgitpatch(patchname)
272
272
273 if dopatch:
273 if dopatch:
274 if dopatch == 'filter':
274 if dopatch == 'filter':
275 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
275 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
276 try:
276 try:
277 files, fuzz = __patch(patchname)
277 files, fuzz = __patch(patchname)
278 finally:
278 finally:
279 if dopatch == 'filter':
279 if dopatch == 'filter':
280 os.unlink(patchname)
280 os.unlink(patchname)
281 else:
281 else:
282 files, fuzz = {}, False
282 files, fuzz = {}, False
283
283
284 for gp in gitpatches:
284 for gp in gitpatches:
285 files[gp.path] = (gp.op, gp)
285 files[gp.path] = (gp.op, gp)
286
286
287 return (files, fuzz)
287 return (files, fuzz)
288
288
289 def diffopts(ui, opts={}):
289 def diffopts(ui, opts={}):
290 return mdiff.diffopts(
290 return mdiff.diffopts(
291 text=opts.get('text'),
291 text=opts.get('text'),
292 git=(opts.get('git') or
292 git=(opts.get('git') or
293 ui.configbool('diff', 'git', None)),
293 ui.configbool('diff', 'git', None)),
294 nodates=(opts.get('nodates') or
294 nodates=(opts.get('nodates') or
295 ui.configbool('diff', 'nodates', None)),
295 ui.configbool('diff', 'nodates', None)),
296 showfunc=(opts.get('show_function') or
296 showfunc=(opts.get('show_function') or
297 ui.configbool('diff', 'showfunc', None)),
297 ui.configbool('diff', 'showfunc', None)),
298 ignorews=(opts.get('ignore_all_space') or
298 ignorews=(opts.get('ignore_all_space') or
299 ui.configbool('diff', 'ignorews', None)),
299 ui.configbool('diff', 'ignorews', None)),
300 ignorewsamount=(opts.get('ignore_space_change') or
300 ignorewsamount=(opts.get('ignore_space_change') or
301 ui.configbool('diff', 'ignorewsamount', None)),
301 ui.configbool('diff', 'ignorewsamount', None)),
302 ignoreblanklines=(opts.get('ignore_blank_lines') or
302 ignoreblanklines=(opts.get('ignore_blank_lines') or
303 ui.configbool('diff', 'ignoreblanklines', None)))
303 ui.configbool('diff', 'ignoreblanklines', None)))
304
304
305 def updatedir(ui, repo, patches, wlock=None):
305 def updatedir(ui, repo, patches, wlock=None):
306 '''Update dirstate after patch application according to metadata'''
306 '''Update dirstate after patch application according to metadata'''
307 if not patches:
307 if not patches:
308 return
308 return
309 copies = []
309 copies = []
310 removes = []
310 removes = []
311 cfiles = patches.keys()
311 cfiles = patches.keys()
312 cwd = repo.getcwd()
312 cwd = repo.getcwd()
313 if cwd:
313 if cwd:
314 cfiles = [util.pathto(cwd, f) for f in patches.keys()]
314 cfiles = [util.pathto(cwd, f) for f in patches.keys()]
315 for f in patches:
315 for f in patches:
316 ctype, gp = patches[f]
316 ctype, gp = patches[f]
317 if ctype == 'RENAME':
317 if ctype == 'RENAME':
318 copies.append((gp.oldpath, gp.path, gp.copymod))
318 copies.append((gp.oldpath, gp.path, gp.copymod))
319 removes.append(gp.oldpath)
319 removes.append(gp.oldpath)
320 elif ctype == 'COPY':
320 elif ctype == 'COPY':
321 copies.append((gp.oldpath, gp.path, gp.copymod))
321 copies.append((gp.oldpath, gp.path, gp.copymod))
322 elif ctype == 'DELETE':
322 elif ctype == 'DELETE':
323 removes.append(gp.path)
323 removes.append(gp.path)
324 for src, dst, after in copies:
324 for src, dst, after in copies:
325 if not after:
325 if not after:
326 copyfile(src, dst, repo.root)
326 copyfile(src, dst, repo.root)
327 repo.copy(src, dst, wlock=wlock)
327 repo.copy(src, dst, wlock=wlock)
328 if removes:
328 if removes:
329 repo.remove(removes, True, wlock=wlock)
329 repo.remove(removes, True, wlock=wlock)
330 for f in patches:
330 for f in patches:
331 ctype, gp = patches[f]
331 ctype, gp = patches[f]
332 if gp and gp.mode:
332 if gp and gp.mode:
333 x = gp.mode & 0100 != 0
333 x = gp.mode & 0100 != 0
334 dst = os.path.join(repo.root, gp.path)
334 dst = os.path.join(repo.root, gp.path)
335 util.set_exec(dst, x)
335 util.set_exec(dst, x)
336 cmdutil.addremove(repo, cfiles, wlock=wlock)
336 cmdutil.addremove(repo, cfiles, wlock=wlock)
337 files = patches.keys()
337 files = patches.keys()
338 files.extend([r for r in removes if r not in files])
338 files.extend([r for r in removes if r not in files])
339 files.sort()
339 files.sort()
340
340
341 return files
341 return files
342
342
343 def diff(repo, node1=None, node2=None, files=None, match=util.always,
343 def diff(repo, node1=None, node2=None, files=None, match=util.always,
344 fp=None, changes=None, opts=None):
344 fp=None, changes=None, opts=None):
345 '''print diff of changes to files between two nodes, or node and
345 '''print diff of changes to files between two nodes, or node and
346 working directory.
346 working directory.
347
347
348 if node1 is None, use first dirstate parent instead.
348 if node1 is None, use first dirstate parent instead.
349 if node2 is None, compare node1 with working directory.'''
349 if node2 is None, compare node1 with working directory.'''
350
350
351 if opts is None:
351 if opts is None:
352 opts = mdiff.defaultopts
352 opts = mdiff.defaultopts
353 if fp is None:
353 if fp is None:
354 fp = repo.ui
354 fp = repo.ui
355
355
356 if not node1:
356 if not node1:
357 node1 = repo.dirstate.parents()[0]
357 node1 = repo.dirstate.parents()[0]
358
358
359 clcache = {}
359 clcache = {}
360 def getchangelog(n):
360 def getchangelog(n):
361 if n not in clcache:
361 if n not in clcache:
362 clcache[n] = repo.changelog.read(n)
362 clcache[n] = repo.changelog.read(n)
363 return clcache[n]
363 return clcache[n]
364 mcache = {}
364 mcache = {}
365 def getmanifest(n):
365 def getmanifest(n):
366 if n not in mcache:
366 if n not in mcache:
367 mcache[n] = repo.manifest.read(n)
367 mcache[n] = repo.manifest.read(n)
368 return mcache[n]
368 return mcache[n]
369 fcache = {}
369 fcache = {}
370 def getfile(f):
370 def getfile(f):
371 if f not in fcache:
371 if f not in fcache:
372 fcache[f] = repo.file(f)
372 fcache[f] = repo.file(f)
373 return fcache[f]
373 return fcache[f]
374
374
375 # reading the data for node1 early allows it to play nicely
375 # reading the data for node1 early allows it to play nicely
376 # with repo.status and the revlog cache.
376 # with repo.status and the revlog cache.
377 change = getchangelog(node1)
377 change = getchangelog(node1)
378 mmap = getmanifest(change[0])
378 mmap = getmanifest(change[0])
379 date1 = util.datestr(change[2])
379 date1 = util.datestr(change[2])
380
380
381 if not changes:
381 if not changes:
382 changes = repo.status(node1, node2, files, match=match)[:5]
382 changes = repo.status(node1, node2, files, match=match)[:5]
383 modified, added, removed, deleted, unknown = changes
383 modified, added, removed, deleted, unknown = changes
384 if files:
384 if files:
385 def filterfiles(filters):
385 def filterfiles(filters):
386 l = [x for x in filters if x in files]
386 l = [x for x in filters if x in files]
387
387
388 for t in files:
388 for t in files:
389 if not t.endswith("/"):
389 if not t.endswith("/"):
390 t += "/"
390 t += "/"
391 l += [x for x in filters if x.startswith(t)]
391 l += [x for x in filters if x.startswith(t)]
392 return l
392 return l
393
393
394 modified, added, removed = map(filterfiles, (modified, added, removed))
394 modified, added, removed = map(filterfiles, (modified, added, removed))
395
395
396 if not modified and not added and not removed:
396 if not modified and not added and not removed:
397 return
397 return
398
398
399 def renamedbetween(f, n1, n2):
399 def renamedbetween(f, n1, n2):
400 r1, r2 = map(repo.changelog.rev, (n1, n2))
400 r1, r2 = map(repo.changelog.rev, (n1, n2))
401 src = None
401 src = None
402 while r2 > r1:
402 while r2 > r1:
403 cl = getchangelog(n2)[0]
403 cl = getchangelog(n2)[0]
404 m = getmanifest(cl)
404 m = getmanifest(cl)
405 try:
405 try:
406 src = getfile(f).renamed(m[f])
406 src = getfile(f).renamed(m[f])
407 except KeyError:
407 except KeyError:
408 return None
408 return None
409 if src:
409 if src:
410 f = src[0]
410 f = src[0]
411 n2 = repo.changelog.parents(n2)[0]
411 n2 = repo.changelog.parents(n2)[0]
412 r2 = repo.changelog.rev(n2)
412 r2 = repo.changelog.rev(n2)
413 return src
413 return src
414
414
415 if node2:
415 if node2:
416 change = getchangelog(node2)
416 change = getchangelog(node2)
417 mmap2 = getmanifest(change[0])
417 mmap2 = getmanifest(change[0])
418 _date2 = util.datestr(change[2])
418 _date2 = util.datestr(change[2])
419 def date2(f):
419 def date2(f):
420 return _date2
420 return _date2
421 def read(f):
421 def read(f):
422 return getfile(f).read(mmap2[f])
422 return getfile(f).read(mmap2[f])
423 def renamed(f):
423 def renamed(f):
424 return renamedbetween(f, node1, node2)
424 return renamedbetween(f, node1, node2)
425 else:
425 else:
426 tz = util.makedate()[1]
426 tz = util.makedate()[1]
427 _date2 = util.datestr()
427 _date2 = util.datestr()
428 def date2(f):
428 def date2(f):
429 try:
429 try:
430 return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz))
430 return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz))
431 except OSError, err:
431 except OSError, err:
432 if err.errno != errno.ENOENT: raise
432 if err.errno != errno.ENOENT: raise
433 return _date2
433 return _date2
434 def read(f):
434 def read(f):
435 return repo.wread(f)
435 return repo.wread(f)
436 def renamed(f):
436 def renamed(f):
437 src = repo.dirstate.copied(f)
437 src = repo.dirstate.copied(f)
438 parent = repo.dirstate.parents()[0]
438 parent = repo.dirstate.parents()[0]
439 if src:
439 if src:
440 f = src[0]
440 f = src[0]
441 of = renamedbetween(f, node1, parent)
441 of = renamedbetween(f, node1, parent)
442 if of:
442 if of:
443 return of
443 return of
444 elif src:
444 elif src:
445 cl = getchangelog(parent)[0]
445 cl = getchangelog(parent)[0]
446 return (src, getmanifest(cl)[src])
446 return (src, getmanifest(cl)[src])
447 else:
447 else:
448 return None
448 return None
449
449
450 if repo.ui.quiet:
450 if repo.ui.quiet:
451 r = None
451 r = None
452 else:
452 else:
453 hexfunc = repo.ui.verbose and hex or short
453 hexfunc = repo.ui.verbose and hex or short
454 r = [hexfunc(node) for node in [node1, node2] if node]
454 r = [hexfunc(node) for node in [node1, node2] if node]
455
455
456 if opts.git:
456 if opts.git:
457 copied = {}
457 copied = {}
458 for f in added:
458 for f in added:
459 src = renamed(f)
459 src = renamed(f)
460 if src:
460 if src:
461 copied[f] = src
461 copied[f] = src
462 srcs = [x[1][0] for x in copied.items()]
462 srcs = [x[1][0] for x in copied.items()]
463
463
464 all = modified + added + removed
464 all = modified + added + removed
465 all.sort()
465 all.sort()
466 for f in all:
466 for f in all:
467 to = None
467 to = None
468 tn = None
468 tn = None
469 dodiff = True
469 dodiff = True
470 header = []
470 if f in mmap:
471 if f in mmap:
471 to = getfile(f).read(mmap[f])
472 to = getfile(f).read(mmap[f])
472 if f not in removed:
473 if f not in removed:
473 tn = read(f)
474 tn = read(f)
474 if opts.git:
475 if opts.git:
475 def gitmode(x):
476 def gitmode(x):
476 return x and '100755' or '100644'
477 return x and '100755' or '100644'
477 def addmodehdr(header, omode, nmode):
478 def addmodehdr(header, omode, nmode):
478 if omode != nmode:
479 if omode != nmode:
479 header.append('old mode %s\n' % omode)
480 header.append('old mode %s\n' % omode)
480 header.append('new mode %s\n' % nmode)
481 header.append('new mode %s\n' % nmode)
481
482
482 a, b = f, f
483 a, b = f, f
483 header = []
484 if f in added:
484 if f in added:
485 if node2:
485 if node2:
486 mode = gitmode(mmap2.execf(f))
486 mode = gitmode(mmap2.execf(f))
487 else:
487 else:
488 mode = gitmode(util.is_exec(repo.wjoin(f), None))
488 mode = gitmode(util.is_exec(repo.wjoin(f), None))
489 if f in copied:
489 if f in copied:
490 a, arev = copied[f]
490 a, arev = copied[f]
491 omode = gitmode(mmap.execf(a))
491 omode = gitmode(mmap.execf(a))
492 addmodehdr(header, omode, mode)
492 addmodehdr(header, omode, mode)
493 op = a in removed and 'rename' or 'copy'
493 op = a in removed and 'rename' or 'copy'
494 header.append('%s from %s\n' % (op, a))
494 header.append('%s from %s\n' % (op, a))
495 header.append('%s to %s\n' % (op, f))
495 header.append('%s to %s\n' % (op, f))
496 to = getfile(a).read(arev)
496 to = getfile(a).read(arev)
497 else:
497 else:
498 header.append('new file mode %s\n' % mode)
498 header.append('new file mode %s\n' % mode)
499 elif f in removed:
499 elif f in removed:
500 if f in srcs:
500 if f in srcs:
501 dodiff = False
501 dodiff = False
502 else:
502 else:
503 mode = gitmode(mmap.execf(f))
503 mode = gitmode(mmap.execf(f))
504 header.append('deleted file mode %s\n' % mode)
504 header.append('deleted file mode %s\n' % mode)
505 else:
505 else:
506 omode = gitmode(mmap.execf(f))
506 omode = gitmode(mmap.execf(f))
507 if node2:
507 if node2:
508 nmode = gitmode(mmap2.execf(f))
508 nmode = gitmode(mmap2.execf(f))
509 else:
509 else:
510 nmode = gitmode(util.is_exec(repo.wjoin(f), mmap.execf(f)))
510 nmode = gitmode(util.is_exec(repo.wjoin(f), mmap.execf(f)))
511 addmodehdr(header, omode, nmode)
511 addmodehdr(header, omode, nmode)
512 r = None
512 r = None
513 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
513 if dodiff:
514 if dodiff:
514 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
515 text = mdiff.unidiff(to, date1, tn, date2(f), f, r, opts=opts)
516 if text or len(header) > 1:
515 fp.write(''.join(header))
517 fp.write(''.join(header))
516 if dodiff:
518 fp.write(text)
517 fp.write(mdiff.unidiff(to, date1, tn, date2(f), f, r, opts=opts))
518
519
519 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
520 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
520 opts=None):
521 opts=None):
521 '''export changesets as hg patches.'''
522 '''export changesets as hg patches.'''
522
523
523 total = len(revs)
524 total = len(revs)
524 revwidth = max(map(len, revs))
525 revwidth = max(map(len, revs))
525
526
526 def single(node, seqno, fp):
527 def single(node, seqno, fp):
527 parents = [p for p in repo.changelog.parents(node) if p != nullid]
528 parents = [p for p in repo.changelog.parents(node) if p != nullid]
528 if switch_parent:
529 if switch_parent:
529 parents.reverse()
530 parents.reverse()
530 prev = (parents and parents[0]) or nullid
531 prev = (parents and parents[0]) or nullid
531 change = repo.changelog.read(node)
532 change = repo.changelog.read(node)
532
533
533 if not fp:
534 if not fp:
534 fp = cmdutil.make_file(repo, template, node, total=total,
535 fp = cmdutil.make_file(repo, template, node, total=total,
535 seqno=seqno, revwidth=revwidth)
536 seqno=seqno, revwidth=revwidth)
536 if fp not in (sys.stdout, repo.ui):
537 if fp not in (sys.stdout, repo.ui):
537 repo.ui.note("%s\n" % fp.name)
538 repo.ui.note("%s\n" % fp.name)
538
539
539 fp.write("# HG changeset patch\n")
540 fp.write("# HG changeset patch\n")
540 fp.write("# User %s\n" % change[1])
541 fp.write("# User %s\n" % change[1])
541 fp.write("# Date %d %d\n" % change[2])
542 fp.write("# Date %d %d\n" % change[2])
542 fp.write("# Node ID %s\n" % hex(node))
543 fp.write("# Node ID %s\n" % hex(node))
543 fp.write("# Parent %s\n" % hex(prev))
544 fp.write("# Parent %s\n" % hex(prev))
544 if len(parents) > 1:
545 if len(parents) > 1:
545 fp.write("# Parent %s\n" % hex(parents[1]))
546 fp.write("# Parent %s\n" % hex(parents[1]))
546 fp.write(change[4].rstrip())
547 fp.write(change[4].rstrip())
547 fp.write("\n\n")
548 fp.write("\n\n")
548
549
549 diff(repo, prev, node, fp=fp, opts=opts)
550 diff(repo, prev, node, fp=fp, opts=opts)
550 if fp not in (sys.stdout, repo.ui):
551 if fp not in (sys.stdout, repo.ui):
551 fp.close()
552 fp.close()
552
553
553 for seqno, cset in enumerate(revs):
554 for seqno, cset in enumerate(revs):
554 single(cset, seqno, fp)
555 single(cset, seqno, fp)
555
556
556 def diffstat(patchlines):
557 def diffstat(patchlines):
557 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
558 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
558 try:
559 try:
559 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
560 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
560 try:
561 try:
561 for line in patchlines: print >> p.tochild, line
562 for line in patchlines: print >> p.tochild, line
562 p.tochild.close()
563 p.tochild.close()
563 if p.wait(): return
564 if p.wait(): return
564 fp = os.fdopen(fd, 'r')
565 fp = os.fdopen(fd, 'r')
565 stat = []
566 stat = []
566 for line in fp: stat.append(line.lstrip())
567 for line in fp: stat.append(line.lstrip())
567 last = stat.pop()
568 last = stat.pop()
568 stat.insert(0, last)
569 stat.insert(0, last)
569 stat = ''.join(stat)
570 stat = ''.join(stat)
570 if stat.startswith('0 files'): raise ValueError
571 if stat.startswith('0 files'): raise ValueError
571 return stat
572 return stat
572 except: raise
573 except: raise
573 finally:
574 finally:
574 try: os.unlink(name)
575 try: os.unlink(name)
575 except: pass
576 except: pass
General Comments 0
You need to be logged in to leave comments. Login now