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)