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)