Mercurial > hg
comparison mercurial/patch.py @ 2956:6dddcba7596a
merge.
author | Vadim Gelfer <vadim.gelfer@gmail.com> |
---|---|
date | Fri, 18 Aug 2006 21:17:28 -0700 |
parents | 6ba3409f9725 |
children | efd26ceedafb |
comparison
equal
deleted
inserted
replaced
2955:9d1c3529ebbc | 2956:6dddcba7596a |
---|---|
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 errno os re shutil sys tempfile") | |
13 | |
14 # helper functions | |
15 | |
16 def copyfile(src, dst, basedir=None): | |
17 if not basedir: | |
18 basedir = os.getcwd() | |
19 | |
20 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)] | |
21 if os.path.exists(absdst): | |
22 raise util.Abort(_("cannot create %s: destination already exists") % | |
23 dst) | |
24 | |
25 targetdir = os.path.dirname(absdst) | |
26 if not os.path.isdir(targetdir): | |
27 os.makedirs(targetdir) | |
28 try: | |
29 shutil.copyfile(abssrc, absdst) | |
30 shutil.copymode(abssrc, absdst) | |
31 except shutil.Error, inst: | |
32 raise util.Abort(str(inst)) | |
33 | |
34 # public functions | |
35 | |
36 def extract(ui, fileobj): | |
37 '''extract patch from data read from fileobj. | |
38 | |
39 patch can be normal patch or contained in email message. | |
40 | |
41 return tuple (filename, message, user, date). any item in returned | |
42 tuple can be None. if filename is None, fileobj did not contain | |
43 patch. caller must unlink filename when done.''' | |
44 | |
45 # attempt to detect the start of a patch | |
46 # (this heuristic is borrowed from quilt) | |
47 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' + | |
48 'retrieving revision [0-9]+(\.[0-9]+)*$|' + | |
49 '(---|\*\*\*)[ \t])', re.MULTILINE) | |
50 | |
51 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-') | |
52 tmpfp = os.fdopen(fd, 'w') | |
53 try: | |
54 hgpatch = False | |
55 | |
56 msg = email.Parser.Parser().parse(fileobj) | |
57 | |
58 message = msg['Subject'] | |
59 user = msg['From'] | |
60 # should try to parse msg['Date'] | |
61 date = None | |
62 | |
63 if message: | |
64 message = message.replace('\n\t', ' ') | |
65 ui.debug('Subject: %s\n' % message) | |
66 if user: | |
67 ui.debug('From: %s\n' % user) | |
68 diffs_seen = 0 | |
69 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch') | |
70 | |
71 for part in msg.walk(): | |
72 content_type = part.get_content_type() | |
73 ui.debug('Content-Type: %s\n' % content_type) | |
74 if content_type not in ok_types: | |
75 continue | |
76 payload = part.get_payload(decode=True) | |
77 m = diffre.search(payload) | |
78 if m: | |
79 ui.debug(_('found patch at byte %d\n') % m.start(0)) | |
80 diffs_seen += 1 | |
81 cfp = cStringIO.StringIO() | |
82 if message: | |
83 cfp.write(message) | |
84 cfp.write('\n') | |
85 for line in payload[:m.start(0)].splitlines(): | |
86 if line.startswith('# HG changeset patch'): | |
87 ui.debug(_('patch generated by hg export\n')) | |
88 hgpatch = True | |
89 # drop earlier commit message content | |
90 cfp.seek(0) | |
91 cfp.truncate() | |
92 elif hgpatch: | |
93 if line.startswith('# User '): | |
94 user = line[7:] | |
95 ui.debug('From: %s\n' % user) | |
96 elif line.startswith("# Date "): | |
97 date = line[7:] | |
98 if not line.startswith('# '): | |
99 cfp.write(line) | |
100 cfp.write('\n') | |
101 message = cfp.getvalue() | |
102 if tmpfp: | |
103 tmpfp.write(payload) | |
104 if not payload.endswith('\n'): | |
105 tmpfp.write('\n') | |
106 elif not diffs_seen and message and content_type == 'text/plain': | |
107 message += '\n' + payload | |
108 except: | |
109 tmpfp.close() | |
110 os.unlink(tmpname) | |
111 raise | |
112 | |
113 tmpfp.close() | |
114 if not diffs_seen: | |
115 os.unlink(tmpname) | |
116 return None, message, user, date | |
117 return tmpname, message, user, date | |
118 | |
119 def readgitpatch(patchname): | |
120 """extract git-style metadata about patches from <patchname>""" | |
121 class gitpatch: | |
122 "op is one of ADD, DELETE, RENAME, MODIFY or COPY" | |
123 def __init__(self, path): | |
124 self.path = path | |
125 self.oldpath = None | |
126 self.mode = None | |
127 self.op = 'MODIFY' | |
128 self.copymod = False | |
129 self.lineno = 0 | |
130 | |
131 # Filter patch for git information | |
132 gitre = re.compile('diff --git a/(.*) b/(.*)') | |
133 pf = file(patchname) | |
134 gp = None | |
135 gitpatches = [] | |
136 # Can have a git patch with only metadata, causing patch to complain | |
137 dopatch = False | |
138 | |
139 lineno = 0 | |
140 for line in pf: | |
141 lineno += 1 | |
142 if line.startswith('diff --git'): | |
143 m = gitre.match(line) | |
144 if m: | |
145 if gp: | |
146 gitpatches.append(gp) | |
147 src, dst = m.group(1,2) | |
148 gp = gitpatch(dst) | |
149 gp.lineno = lineno | |
150 elif gp: | |
151 if line.startswith('--- '): | |
152 if gp.op in ('COPY', 'RENAME'): | |
153 gp.copymod = True | |
154 dopatch = 'filter' | |
155 gitpatches.append(gp) | |
156 gp = None | |
157 if not dopatch: | |
158 dopatch = True | |
159 continue | |
160 if line.startswith('rename from '): | |
161 gp.op = 'RENAME' | |
162 gp.oldpath = line[12:].rstrip() | |
163 elif line.startswith('rename to '): | |
164 gp.path = line[10:].rstrip() | |
165 elif line.startswith('copy from '): | |
166 gp.op = 'COPY' | |
167 gp.oldpath = line[10:].rstrip() | |
168 elif line.startswith('copy to '): | |
169 gp.path = line[8:].rstrip() | |
170 elif line.startswith('deleted file'): | |
171 gp.op = 'DELETE' | |
172 elif line.startswith('new file mode '): | |
173 gp.op = 'ADD' | |
174 gp.mode = int(line.rstrip()[-3:], 8) | |
175 elif line.startswith('new mode '): | |
176 gp.mode = int(line.rstrip()[-3:], 8) | |
177 if gp: | |
178 gitpatches.append(gp) | |
179 | |
180 if not gitpatches: | |
181 dopatch = True | |
182 | |
183 return (dopatch, gitpatches) | |
184 | |
185 def dogitpatch(patchname, gitpatches): | |
186 """Preprocess git patch so that vanilla patch can handle it""" | |
187 pf = file(patchname) | |
188 pfline = 1 | |
189 | |
190 fd, patchname = tempfile.mkstemp(prefix='hg-patch-') | |
191 tmpfp = os.fdopen(fd, 'w') | |
192 | |
193 try: | |
194 for i in range(len(gitpatches)): | |
195 p = gitpatches[i] | |
196 if not p.copymod: | |
197 continue | |
198 | |
199 copyfile(p.oldpath, p.path) | |
200 | |
201 # rewrite patch hunk | |
202 while pfline < p.lineno: | |
203 tmpfp.write(pf.readline()) | |
204 pfline += 1 | |
205 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path)) | |
206 line = pf.readline() | |
207 pfline += 1 | |
208 while not line.startswith('--- a/'): | |
209 tmpfp.write(line) | |
210 line = pf.readline() | |
211 pfline += 1 | |
212 tmpfp.write('--- a/%s\n' % p.path) | |
213 | |
214 line = pf.readline() | |
215 while line: | |
216 tmpfp.write(line) | |
217 line = pf.readline() | |
218 except: | |
219 tmpfp.close() | |
220 os.unlink(patchname) | |
221 raise | |
222 | |
223 tmpfp.close() | |
224 return patchname | |
225 | |
226 def patch(patchname, ui, strip=1, cwd=None): | |
227 """apply the patch <patchname> to the working directory. | |
228 a list of patched files is returned""" | |
229 | |
230 (dopatch, gitpatches) = readgitpatch(patchname) | |
231 | |
232 files = {} | |
233 fuzz = False | |
234 if dopatch: | |
235 if dopatch == 'filter': | |
236 patchname = dogitpatch(patchname, gitpatches) | |
237 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''), 'patch') | |
238 args = [] | |
239 if cwd: | |
240 args.append('-d %s' % util.shellquote(cwd)) | |
241 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip, | |
242 util.shellquote(patchname))) | |
243 | |
244 if dopatch == 'filter': | |
245 False and os.unlink(patchname) | |
246 | |
247 for line in fp: | |
248 line = line.rstrip() | |
249 ui.note(line + '\n') | |
250 if line.startswith('patching file '): | |
251 pf = util.parse_patch_output(line) | |
252 printed_file = False | |
253 files.setdefault(pf, (None, None)) | |
254 elif line.find('with fuzz') >= 0: | |
255 fuzz = True | |
256 if not printed_file: | |
257 ui.warn(pf + '\n') | |
258 printed_file = True | |
259 ui.warn(line + '\n') | |
260 elif line.find('saving rejects to file') >= 0: | |
261 ui.warn(line + '\n') | |
262 elif line.find('FAILED') >= 0: | |
263 if not printed_file: | |
264 ui.warn(pf + '\n') | |
265 printed_file = True | |
266 ui.warn(line + '\n') | |
267 | |
268 code = fp.close() | |
269 if code: | |
270 raise util.Abort(_("patch command failed: %s") % | |
271 util.explain_exit(code)[0]) | |
272 | |
273 for gp in gitpatches: | |
274 files[gp.path] = (gp.op, gp) | |
275 | |
276 return (files, fuzz) | |
277 | |
278 def diffopts(ui, opts={}): | |
279 return mdiff.diffopts( | |
280 text=opts.get('text'), | |
281 git=(opts.get('git') or | |
282 ui.configbool('diff', 'git', None)), | |
283 showfunc=(opts.get('show_function') or | |
284 ui.configbool('diff', 'showfunc', None)), | |
285 ignorews=(opts.get('ignore_all_space') or | |
286 ui.configbool('diff', 'ignorews', None)), | |
287 ignorewsamount=(opts.get('ignore_space_change') or | |
288 ui.configbool('diff', 'ignorewsamount', None)), | |
289 ignoreblanklines=(opts.get('ignore_blank_lines') or | |
290 ui.configbool('diff', 'ignoreblanklines', None))) | |
291 | |
292 def updatedir(ui, repo, patches, wlock=None): | |
293 '''Update dirstate after patch application according to metadata''' | |
294 if not patches: | |
295 return | |
296 copies = [] | |
297 removes = [] | |
298 cfiles = patches.keys() | |
299 copts = {'after': False, 'force': False} | |
300 cwd = repo.getcwd() | |
301 if cwd: | |
302 cfiles = [util.pathto(cwd, f) for f in patches.keys()] | |
303 for f in patches: | |
304 ctype, gp = patches[f] | |
305 if ctype == 'RENAME': | |
306 copies.append((gp.oldpath, gp.path, gp.copymod)) | |
307 removes.append(gp.oldpath) | |
308 elif ctype == 'COPY': | |
309 copies.append((gp.oldpath, gp.path, gp.copymod)) | |
310 elif ctype == 'DELETE': | |
311 removes.append(gp.path) | |
312 for src, dst, after in copies: | |
313 if not after: | |
314 copyfile(src, dst, repo.root) | |
315 repo.copy(src, dst, wlock=wlock) | |
316 if removes: | |
317 repo.remove(removes, True, wlock=wlock) | |
318 for f in patches: | |
319 ctype, gp = patches[f] | |
320 if gp and gp.mode: | |
321 x = gp.mode & 0100 != 0 | |
322 dst = os.path.join(repo.root, gp.path) | |
323 util.set_exec(dst, x) | |
324 cmdutil.addremove(repo, cfiles, wlock=wlock) | |
325 files = patches.keys() | |
326 files.extend([r for r in removes if r not in files]) | |
327 files.sort() | |
328 | |
329 return files | |
330 | |
331 def diff(repo, node1=None, node2=None, files=None, match=util.always, | |
332 fp=None, changes=None, opts=None): | |
333 '''print diff of changes to files between two nodes, or node and | |
334 working directory. | |
335 | |
336 if node1 is None, use first dirstate parent instead. | |
337 if node2 is None, compare node1 with working directory.''' | |
338 | |
339 if opts is None: | |
340 opts = mdiff.defaultopts | |
341 if fp is None: | |
342 fp = repo.ui | |
343 | |
344 if not node1: | |
345 node1 = repo.dirstate.parents()[0] | |
346 | |
347 clcache = {} | |
348 def getchangelog(n): | |
349 if n not in clcache: | |
350 clcache[n] = repo.changelog.read(n) | |
351 return clcache[n] | |
352 mcache = {} | |
353 def getmanifest(n): | |
354 if n not in mcache: | |
355 mcache[n] = repo.manifest.read(n) | |
356 return mcache[n] | |
357 fcache = {} | |
358 def getfile(f): | |
359 if f not in fcache: | |
360 fcache[f] = repo.file(f) | |
361 return fcache[f] | |
362 | |
363 # reading the data for node1 early allows it to play nicely | |
364 # with repo.status and the revlog cache. | |
365 change = getchangelog(node1) | |
366 mmap = getmanifest(change[0]) | |
367 date1 = util.datestr(change[2]) | |
368 | |
369 if not changes: | |
370 changes = repo.status(node1, node2, files, match=match)[:5] | |
371 modified, added, removed, deleted, unknown = changes | |
372 if files: | |
373 def filterfiles(filters): | |
374 l = [x for x in filters if x in files] | |
375 | |
376 for t in files: | |
377 if not t.endswith("/"): | |
378 t += "/" | |
379 l += [x for x in filters if x.startswith(t)] | |
380 return l | |
381 | |
382 modified, added, removed = map(filterfiles, (modified, added, removed)) | |
383 | |
384 if not modified and not added and not removed: | |
385 return | |
386 | |
387 def renamedbetween(f, n1, n2): | |
388 r1, r2 = map(repo.changelog.rev, (n1, n2)) | |
389 src = None | |
390 while r2 > r1: | |
391 cl = getchangelog(n2)[0] | |
392 m = getmanifest(cl) | |
393 try: | |
394 src = getfile(f).renamed(m[f]) | |
395 except KeyError: | |
396 return None | |
397 if src: | |
398 f = src[0] | |
399 n2 = repo.changelog.parents(n2)[0] | |
400 r2 = repo.changelog.rev(n2) | |
401 return src | |
402 | |
403 if node2: | |
404 change = getchangelog(node2) | |
405 mmap2 = getmanifest(change[0]) | |
406 _date2 = util.datestr(change[2]) | |
407 def date2(f): | |
408 return _date2 | |
409 def read(f): | |
410 return getfile(f).read(mmap2[f]) | |
411 def renamed(f): | |
412 return renamedbetween(f, node1, node2) | |
413 else: | |
414 tz = util.makedate()[1] | |
415 _date2 = util.datestr() | |
416 def date2(f): | |
417 try: | |
418 return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz)) | |
419 except OSError, err: | |
420 if err.errno != errno.ENOENT: raise | |
421 return _date2 | |
422 def read(f): | |
423 return repo.wread(f) | |
424 def renamed(f): | |
425 src = repo.dirstate.copies.get(f) | |
426 parent = repo.dirstate.parents()[0] | |
427 if src: | |
428 f = src[0] | |
429 of = renamedbetween(f, node1, parent) | |
430 if of: | |
431 return of | |
432 elif src: | |
433 cl = getchangelog(parent)[0] | |
434 return (src, getmanifest(cl)[src]) | |
435 else: | |
436 return None | |
437 | |
438 if repo.ui.quiet: | |
439 r = None | |
440 else: | |
441 hexfunc = repo.ui.verbose and hex or short | |
442 r = [hexfunc(node) for node in [node1, node2] if node] | |
443 | |
444 if opts.git: | |
445 copied = {} | |
446 for f in added: | |
447 src = renamed(f) | |
448 if src: | |
449 copied[f] = src | |
450 srcs = [x[1][0] for x in copied.items()] | |
451 | |
452 all = modified + added + removed | |
453 all.sort() | |
454 for f in all: | |
455 to = None | |
456 tn = None | |
457 dodiff = True | |
458 if f in mmap: | |
459 to = getfile(f).read(mmap[f]) | |
460 if f not in removed: | |
461 tn = read(f) | |
462 if opts.git: | |
463 def gitmode(x): | |
464 return x and '100755' or '100644' | |
465 def addmodehdr(header, omode, nmode): | |
466 if omode != nmode: | |
467 header.append('old mode %s\n' % omode) | |
468 header.append('new mode %s\n' % nmode) | |
469 | |
470 a, b = f, f | |
471 header = [] | |
472 if f in added: | |
473 if node2: | |
474 mode = gitmode(mmap2.execf(f)) | |
475 else: | |
476 mode = gitmode(util.is_exec(repo.wjoin(f), None)) | |
477 if f in copied: | |
478 a, arev = copied[f] | |
479 omode = gitmode(mmap.execf(a)) | |
480 addmodehdr(header, omode, mode) | |
481 op = a in removed and 'rename' or 'copy' | |
482 header.append('%s from %s\n' % (op, a)) | |
483 header.append('%s to %s\n' % (op, f)) | |
484 to = getfile(a).read(arev) | |
485 else: | |
486 header.append('new file mode %s\n' % mode) | |
487 elif f in removed: | |
488 if f in srcs: | |
489 dodiff = False | |
490 else: | |
491 mode = gitmode(mmap.execf(f)) | |
492 header.append('deleted file mode %s\n' % mode) | |
493 else: | |
494 omode = gitmode(mmap.execf(f)) | |
495 nmode = gitmode(util.is_exec(repo.wjoin(f), mmap.execf(f))) | |
496 addmodehdr(header, omode, nmode) | |
497 r = None | |
498 if dodiff: | |
499 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b)) | |
500 fp.write(''.join(header)) | |
501 if dodiff: | |
502 fp.write(mdiff.unidiff(to, date1, tn, date2(f), f, r, opts=opts)) | |
503 | |
504 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False, | |
505 opts=None): | |
506 '''export changesets as hg patches.''' | |
507 | |
508 total = len(revs) | |
509 revwidth = max(map(len, revs)) | |
510 | |
511 def single(node, seqno, fp): | |
512 parents = [p for p in repo.changelog.parents(node) if p != nullid] | |
513 if switch_parent: | |
514 parents.reverse() | |
515 prev = (parents and parents[0]) or nullid | |
516 change = repo.changelog.read(node) | |
517 | |
518 if not fp: | |
519 fp = cmdutil.make_file(repo, template, node, total=total, | |
520 seqno=seqno, revwidth=revwidth) | |
521 if fp not in (sys.stdout, repo.ui): | |
522 repo.ui.note("%s\n" % fp.name) | |
523 | |
524 fp.write("# HG changeset patch\n") | |
525 fp.write("# User %s\n" % change[1]) | |
526 fp.write("# Date %d %d\n" % change[2]) | |
527 fp.write("# Node ID %s\n" % hex(node)) | |
528 fp.write("# Parent %s\n" % hex(prev)) | |
529 if len(parents) > 1: | |
530 fp.write("# Parent %s\n" % hex(parents[1])) | |
531 fp.write(change[4].rstrip()) | |
532 fp.write("\n\n") | |
533 | |
534 diff(repo, prev, node, fp=fp, opts=opts) | |
535 if fp not in (sys.stdout, repo.ui): | |
536 fp.close() | |
537 | |
538 for seqno, cset in enumerate(revs): | |
539 single(cset, seqno, fp) |