Mercurial > hg > mercurial-crew-with-dirclash
comparison mercurial/patch.py @ 2899:bee4b7abcb01
Merge with crew
author | Matt Mackall <mpm@selenic.com> |
---|---|
date | Mon, 14 Aug 2006 14:42:15 -0500 |
parents | eab07a7b7491 |
children | 8b02af865990 3848488244fc |
comparison
equal
deleted
inserted
replaced
2898:06c05c675a35 | 2899:bee4b7abcb01 |
---|---|
1 # patch.py - patch file parsing routines | |
2 # | |
3 # Copyright 2006 Brendan Cully <brendan@kublai.com> | |
4 # | |
5 # This software may be used and distributed according to the terms | |
6 # of the GNU General Public License, incorporated herein by reference. | |
7 | |
8 from demandload import demandload | |
9 from i18n import gettext as _ | |
10 from node import * | |
11 demandload(globals(), "cmdutil mdiff util") | |
12 demandload(globals(), "cStringIO email.Parser os re shutil sys tempfile") | |
13 | |
14 def extract(ui, fileobj): | |
15 '''extract patch from data read from fileobj. | |
16 | |
17 patch can be normal patch or contained in email message. | |
18 | |
19 return tuple (filename, message, user, date). any item in returned | |
20 tuple can be None. if filename is None, fileobj did not contain | |
21 patch. caller must unlink filename when done.''' | |
22 | |
23 # attempt to detect the start of a patch | |
24 # (this heuristic is borrowed from quilt) | |
25 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' + | |
26 'retrieving revision [0-9]+(\.[0-9]+)*$|' + | |
27 '(---|\*\*\*)[ \t])', re.MULTILINE) | |
28 | |
29 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-') | |
30 tmpfp = os.fdopen(fd, 'w') | |
31 try: | |
32 hgpatch = False | |
33 | |
34 msg = email.Parser.Parser().parse(fileobj) | |
35 | |
36 message = msg['Subject'] | |
37 user = msg['From'] | |
38 # should try to parse msg['Date'] | |
39 date = None | |
40 | |
41 if message: | |
42 message = message.replace('\n\t', ' ') | |
43 ui.debug('Subject: %s\n' % message) | |
44 if user: | |
45 ui.debug('From: %s\n' % user) | |
46 diffs_seen = 0 | |
47 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch') | |
48 | |
49 for part in msg.walk(): | |
50 content_type = part.get_content_type() | |
51 ui.debug('Content-Type: %s\n' % content_type) | |
52 if content_type not in ok_types: | |
53 continue | |
54 payload = part.get_payload(decode=True) | |
55 m = diffre.search(payload) | |
56 if m: | |
57 ui.debug(_('found patch at byte %d\n') % m.start(0)) | |
58 diffs_seen += 1 | |
59 cfp = cStringIO.StringIO() | |
60 if message: | |
61 cfp.write(message) | |
62 cfp.write('\n') | |
63 for line in payload[:m.start(0)].splitlines(): | |
64 if line.startswith('# HG changeset patch'): | |
65 ui.debug(_('patch generated by hg export\n')) | |
66 hgpatch = True | |
67 # drop earlier commit message content | |
68 cfp.seek(0) | |
69 cfp.truncate() | |
70 elif hgpatch: | |
71 if line.startswith('# User '): | |
72 user = line[7:] | |
73 ui.debug('From: %s\n' % user) | |
74 elif line.startswith("# Date "): | |
75 date = line[7:] | |
76 if not line.startswith('# '): | |
77 cfp.write(line) | |
78 cfp.write('\n') | |
79 message = cfp.getvalue() | |
80 if tmpfp: | |
81 tmpfp.write(payload) | |
82 if not payload.endswith('\n'): | |
83 tmpfp.write('\n') | |
84 elif not diffs_seen and message and content_type == 'text/plain': | |
85 message += '\n' + payload | |
86 except: | |
87 tmpfp.close() | |
88 os.unlink(tmpname) | |
89 raise | |
90 | |
91 tmpfp.close() | |
92 if not diffs_seen: | |
93 os.unlink(tmpname) | |
94 return None, message, user, date | |
95 return tmpname, message, user, date | |
96 | |
97 def readgitpatch(patchname): | |
98 """extract git-style metadata about patches from <patchname>""" | |
99 class gitpatch: | |
100 "op is one of ADD, DELETE, RENAME, MODIFY or COPY" | |
101 def __init__(self, path): | |
102 self.path = path | |
103 self.oldpath = None | |
104 self.mode = None | |
105 self.op = 'MODIFY' | |
106 self.copymod = False | |
107 self.lineno = 0 | |
108 | |
109 # Filter patch for git information | |
110 gitre = re.compile('diff --git a/(.*) b/(.*)') | |
111 pf = file(patchname) | |
112 gp = None | |
113 gitpatches = [] | |
114 # Can have a git patch with only metadata, causing patch to complain | |
115 dopatch = False | |
116 | |
117 lineno = 0 | |
118 for line in pf: | |
119 lineno += 1 | |
120 if line.startswith('diff --git'): | |
121 m = gitre.match(line) | |
122 if m: | |
123 if gp: | |
124 gitpatches.append(gp) | |
125 src, dst = m.group(1,2) | |
126 gp = gitpatch(dst) | |
127 gp.lineno = lineno | |
128 elif gp: | |
129 if line.startswith('--- '): | |
130 if gp.op in ('COPY', 'RENAME'): | |
131 gp.copymod = True | |
132 dopatch = 'filter' | |
133 gitpatches.append(gp) | |
134 gp = None | |
135 if not dopatch: | |
136 dopatch = True | |
137 continue | |
138 if line.startswith('rename from '): | |
139 gp.op = 'RENAME' | |
140 gp.oldpath = line[12:].rstrip() | |
141 elif line.startswith('rename to '): | |
142 gp.path = line[10:].rstrip() | |
143 elif line.startswith('copy from '): | |
144 gp.op = 'COPY' | |
145 gp.oldpath = line[10:].rstrip() | |
146 elif line.startswith('copy to '): | |
147 gp.path = line[8:].rstrip() | |
148 elif line.startswith('deleted file'): | |
149 gp.op = 'DELETE' | |
150 elif line.startswith('new file mode '): | |
151 gp.op = 'ADD' | |
152 gp.mode = int(line.rstrip()[-3:], 8) | |
153 elif line.startswith('new mode '): | |
154 gp.mode = int(line.rstrip()[-3:], 8) | |
155 if gp: | |
156 gitpatches.append(gp) | |
157 | |
158 if not gitpatches: | |
159 dopatch = True | |
160 | |
161 return (dopatch, gitpatches) | |
162 | |
163 def dogitpatch(patchname, gitpatches): | |
164 """Preprocess git patch so that vanilla patch can handle it""" | |
165 pf = file(patchname) | |
166 pfline = 1 | |
167 | |
168 fd, patchname = tempfile.mkstemp(prefix='hg-patch-') | |
169 tmpfp = os.fdopen(fd, 'w') | |
170 | |
171 try: | |
172 for i in range(len(gitpatches)): | |
173 p = gitpatches[i] | |
174 if not p.copymod: | |
175 continue | |
176 | |
177 if os.path.exists(p.path): | |
178 raise util.Abort(_("cannot create %s: destination already exists") % | |
179 p.path) | |
180 | |
181 (src, dst) = [os.path.join(os.getcwd(), n) | |
182 for n in (p.oldpath, p.path)] | |
183 | |
184 targetdir = os.path.dirname(dst) | |
185 if not os.path.isdir(targetdir): | |
186 os.makedirs(targetdir) | |
187 try: | |
188 shutil.copyfile(src, dst) | |
189 shutil.copymode(src, dst) | |
190 except shutil.Error, inst: | |
191 raise util.Abort(str(inst)) | |
192 | |
193 # rewrite patch hunk | |
194 while pfline < p.lineno: | |
195 tmpfp.write(pf.readline()) | |
196 pfline += 1 | |
197 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path)) | |
198 line = pf.readline() | |
199 pfline += 1 | |
200 while not line.startswith('--- a/'): | |
201 tmpfp.write(line) | |
202 line = pf.readline() | |
203 pfline += 1 | |
204 tmpfp.write('--- a/%s\n' % p.path) | |
205 | |
206 line = pf.readline() | |
207 while line: | |
208 tmpfp.write(line) | |
209 line = pf.readline() | |
210 except: | |
211 tmpfp.close() | |
212 os.unlink(patchname) | |
213 raise | |
214 | |
215 tmpfp.close() | |
216 return patchname | |
217 | |
218 def patch(strip, patchname, ui, cwd=None): | |
219 """apply the patch <patchname> to the working directory. | |
220 a list of patched files is returned""" | |
221 | |
222 (dopatch, gitpatches) = readgitpatch(patchname) | |
223 | |
224 files = {} | |
225 if dopatch: | |
226 if dopatch == 'filter': | |
227 patchname = dogitpatch(patchname, gitpatches) | |
228 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''), 'patch') | |
229 args = [] | |
230 if cwd: | |
231 args.append('-d %s' % util.shellquote(cwd)) | |
232 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip, | |
233 util.shellquote(patchname))) | |
234 | |
235 if dopatch == 'filter': | |
236 False and os.unlink(patchname) | |
237 | |
238 for line in fp: | |
239 line = line.rstrip() | |
240 ui.status("%s\n" % line) | |
241 if line.startswith('patching file '): | |
242 pf = util.parse_patch_output(line) | |
243 files.setdefault(pf, (None, None)) | |
244 code = fp.close() | |
245 if code: | |
246 raise util.Abort(_("patch command failed: %s") % | |
247 util.explain_exit(code)[0]) | |
248 | |
249 for gp in gitpatches: | |
250 files[gp.path] = (gp.op, gp) | |
251 | |
252 return files | |
253 | |
254 def diff(repo, node1=None, node2=None, files=None, match=util.always, | |
255 fp=None, changes=None, opts=None): | |
256 '''print diff of changes to files between two nodes, or node and | |
257 working directory. | |
258 | |
259 if node1 is None, use first dirstate parent instead. | |
260 if node2 is None, compare node1 with working directory.''' | |
261 | |
262 if opts is None: | |
263 opts = mdiff.defaultopts | |
264 if fp is None: | |
265 fp = repo.ui | |
266 | |
267 if not node1: | |
268 node1 = repo.dirstate.parents()[0] | |
269 # reading the data for node1 early allows it to play nicely | |
270 # with repo.status and the revlog cache. | |
271 change = repo.changelog.read(node1) | |
272 mmap = repo.manifest.read(change[0]) | |
273 date1 = util.datestr(change[2]) | |
274 | |
275 if not changes: | |
276 changes = repo.status(node1, node2, files, match=match)[:5] | |
277 modified, added, removed, deleted, unknown = changes | |
278 if files: | |
279 def filterfiles(filters): | |
280 l = [x for x in filters if x in files] | |
281 | |
282 for t in files: | |
283 if not t.endswith("/"): | |
284 t += "/" | |
285 l += [x for x in filters if x.startswith(t)] | |
286 return l | |
287 | |
288 modified, added, removed = map(filterfiles, (modified, added, removed)) | |
289 | |
290 if not modified and not added and not removed: | |
291 return | |
292 | |
293 if node2: | |
294 change = repo.changelog.read(node2) | |
295 mmap2 = repo.manifest.read(change[0]) | |
296 _date2 = util.datestr(change[2]) | |
297 def date2(f): | |
298 return _date2 | |
299 def read(f): | |
300 return repo.file(f).read(mmap2[f]) | |
301 else: | |
302 tz = util.makedate()[1] | |
303 _date2 = util.datestr() | |
304 def date2(f): | |
305 try: | |
306 return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz)) | |
307 except OSError, err: | |
308 if err.errno != errno.ENOENT: raise | |
309 return _date2 | |
310 def read(f): | |
311 return repo.wread(f) | |
312 | |
313 if repo.ui.quiet: | |
314 r = None | |
315 else: | |
316 hexfunc = repo.ui.verbose and hex or short | |
317 r = [hexfunc(node) for node in [node1, node2] if node] | |
318 | |
319 all = modified + added + removed | |
320 all.sort() | |
321 for f in all: | |
322 to = None | |
323 tn = None | |
324 if f in mmap: | |
325 to = repo.file(f).read(mmap[f]) | |
326 if f not in removed: | |
327 tn = read(f) | |
328 fp.write(mdiff.unidiff(to, date1, tn, date2(f), f, r, opts=opts)) | |
329 | |
330 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False, | |
331 opts=None): | |
332 '''export changesets as hg patches.''' | |
333 | |
334 total = len(revs) | |
335 revwidth = max(map(len, revs)) | |
336 | |
337 def single(node, seqno, fp): | |
338 parents = [p for p in repo.changelog.parents(node) if p != nullid] | |
339 if switch_parent: | |
340 parents.reverse() | |
341 prev = (parents and parents[0]) or nullid | |
342 change = repo.changelog.read(node) | |
343 | |
344 if not fp: | |
345 fp = cmdutil.make_file(repo, template, node, total=total, | |
346 seqno=seqno, revwidth=revwidth) | |
347 if fp not in (sys.stdout, repo.ui): | |
348 repo.ui.note("%s\n" % fp.name) | |
349 | |
350 fp.write("# HG changeset patch\n") | |
351 fp.write("# User %s\n" % change[1]) | |
352 fp.write("# Date %d %d\n" % change[2]) | |
353 fp.write("# Node ID %s\n" % hex(node)) | |
354 fp.write("# Parent %s\n" % hex(prev)) | |
355 if len(parents) > 1: | |
356 fp.write("# Parent %s\n" % hex(parents[1])) | |
357 fp.write(change[4].rstrip()) | |
358 fp.write("\n\n") | |
359 | |
360 diff(repo, prev, node, fp=fp, opts=opts) | |
361 if fp not in (sys.stdout, repo.ui): | |
362 fp.close() | |
363 | |
364 for seqno, cset in enumerate(revs): | |
365 single(cset, seqno, fp) |