##// END OF EJS Templates
patch: move diff parsing in iterhunks generator
Patrick Mezard -
r5650:5d3e2f91 default
parent child Browse files
Show More
@@ -1,1360 +1,1377 b''
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 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 from i18n import _
9 from i18n import _
10 from node import *
10 from node import *
11 import base85, cmdutil, mdiff, util, context, revlog, diffhelpers
11 import base85, cmdutil, mdiff, util, context, revlog, diffhelpers
12 import cStringIO, email.Parser, os, popen2, re, sha, errno
12 import cStringIO, email.Parser, os, popen2, re, sha, errno
13 import sys, tempfile, zlib
13 import sys, tempfile, zlib
14
14
15 class PatchError(Exception):
15 class PatchError(Exception):
16 pass
16 pass
17
17
18 class NoHunks(PatchError):
18 class NoHunks(PatchError):
19 pass
19 pass
20
20
21 # helper functions
21 # helper functions
22
22
23 def copyfile(src, dst, basedir=None):
23 def copyfile(src, dst, basedir=None):
24 if not basedir:
24 if not basedir:
25 basedir = os.getcwd()
25 basedir = os.getcwd()
26
26
27 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
27 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
28 if os.path.exists(absdst):
28 if os.path.exists(absdst):
29 raise util.Abort(_("cannot create %s: destination already exists") %
29 raise util.Abort(_("cannot create %s: destination already exists") %
30 dst)
30 dst)
31
31
32 targetdir = os.path.dirname(absdst)
32 targetdir = os.path.dirname(absdst)
33 if not os.path.isdir(targetdir):
33 if not os.path.isdir(targetdir):
34 os.makedirs(targetdir)
34 os.makedirs(targetdir)
35
35
36 util.copyfile(abssrc, absdst)
36 util.copyfile(abssrc, absdst)
37
37
38 # public functions
38 # public functions
39
39
40 def extract(ui, fileobj):
40 def extract(ui, fileobj):
41 '''extract patch from data read from fileobj.
41 '''extract patch from data read from fileobj.
42
42
43 patch can be a normal patch or contained in an email message.
43 patch can be a normal patch or contained in an email message.
44
44
45 return tuple (filename, message, user, date, node, p1, p2).
45 return tuple (filename, message, user, date, node, p1, p2).
46 Any item in the returned tuple can be None. If filename is None,
46 Any item in the returned tuple can be None. If filename is None,
47 fileobj did not contain a patch. Caller must unlink filename when done.'''
47 fileobj did not contain a patch. Caller must unlink filename when done.'''
48
48
49 # attempt to detect the start of a patch
49 # attempt to detect the start of a patch
50 # (this heuristic is borrowed from quilt)
50 # (this heuristic is borrowed from quilt)
51 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
51 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
52 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
52 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
53 '(---|\*\*\*)[ \t])', re.MULTILINE)
53 '(---|\*\*\*)[ \t])', re.MULTILINE)
54
54
55 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
55 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
56 tmpfp = os.fdopen(fd, 'w')
56 tmpfp = os.fdopen(fd, 'w')
57 try:
57 try:
58 msg = email.Parser.Parser().parse(fileobj)
58 msg = email.Parser.Parser().parse(fileobj)
59
59
60 subject = msg['Subject']
60 subject = msg['Subject']
61 user = msg['From']
61 user = msg['From']
62 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
62 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
63 # should try to parse msg['Date']
63 # should try to parse msg['Date']
64 date = None
64 date = None
65 nodeid = None
65 nodeid = None
66 branch = None
66 branch = None
67 parents = []
67 parents = []
68
68
69 if subject:
69 if subject:
70 if subject.startswith('[PATCH'):
70 if subject.startswith('[PATCH'):
71 pend = subject.find(']')
71 pend = subject.find(']')
72 if pend >= 0:
72 if pend >= 0:
73 subject = subject[pend+1:].lstrip()
73 subject = subject[pend+1:].lstrip()
74 subject = subject.replace('\n\t', ' ')
74 subject = subject.replace('\n\t', ' ')
75 ui.debug('Subject: %s\n' % subject)
75 ui.debug('Subject: %s\n' % subject)
76 if user:
76 if user:
77 ui.debug('From: %s\n' % user)
77 ui.debug('From: %s\n' % user)
78 diffs_seen = 0
78 diffs_seen = 0
79 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
79 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
80 message = ''
80 message = ''
81 for part in msg.walk():
81 for part in msg.walk():
82 content_type = part.get_content_type()
82 content_type = part.get_content_type()
83 ui.debug('Content-Type: %s\n' % content_type)
83 ui.debug('Content-Type: %s\n' % content_type)
84 if content_type not in ok_types:
84 if content_type not in ok_types:
85 continue
85 continue
86 payload = part.get_payload(decode=True)
86 payload = part.get_payload(decode=True)
87 m = diffre.search(payload)
87 m = diffre.search(payload)
88 if m:
88 if m:
89 hgpatch = False
89 hgpatch = False
90 ignoretext = False
90 ignoretext = False
91
91
92 ui.debug(_('found patch at byte %d\n') % m.start(0))
92 ui.debug(_('found patch at byte %d\n') % m.start(0))
93 diffs_seen += 1
93 diffs_seen += 1
94 cfp = cStringIO.StringIO()
94 cfp = cStringIO.StringIO()
95 for line in payload[:m.start(0)].splitlines():
95 for line in payload[:m.start(0)].splitlines():
96 if line.startswith('# HG changeset patch'):
96 if line.startswith('# HG changeset patch'):
97 ui.debug(_('patch generated by hg export\n'))
97 ui.debug(_('patch generated by hg export\n'))
98 hgpatch = True
98 hgpatch = True
99 # drop earlier commit message content
99 # drop earlier commit message content
100 cfp.seek(0)
100 cfp.seek(0)
101 cfp.truncate()
101 cfp.truncate()
102 subject = None
102 subject = None
103 elif hgpatch:
103 elif hgpatch:
104 if line.startswith('# User '):
104 if line.startswith('# User '):
105 user = line[7:]
105 user = line[7:]
106 ui.debug('From: %s\n' % user)
106 ui.debug('From: %s\n' % user)
107 elif line.startswith("# Date "):
107 elif line.startswith("# Date "):
108 date = line[7:]
108 date = line[7:]
109 elif line.startswith("# Branch "):
109 elif line.startswith("# Branch "):
110 branch = line[9:]
110 branch = line[9:]
111 elif line.startswith("# Node ID "):
111 elif line.startswith("# Node ID "):
112 nodeid = line[10:]
112 nodeid = line[10:]
113 elif line.startswith("# Parent "):
113 elif line.startswith("# Parent "):
114 parents.append(line[10:])
114 parents.append(line[10:])
115 elif line == '---' and gitsendmail:
115 elif line == '---' and gitsendmail:
116 ignoretext = True
116 ignoretext = True
117 if not line.startswith('# ') and not ignoretext:
117 if not line.startswith('# ') and not ignoretext:
118 cfp.write(line)
118 cfp.write(line)
119 cfp.write('\n')
119 cfp.write('\n')
120 message = cfp.getvalue()
120 message = cfp.getvalue()
121 if tmpfp:
121 if tmpfp:
122 tmpfp.write(payload)
122 tmpfp.write(payload)
123 if not payload.endswith('\n'):
123 if not payload.endswith('\n'):
124 tmpfp.write('\n')
124 tmpfp.write('\n')
125 elif not diffs_seen and message and content_type == 'text/plain':
125 elif not diffs_seen and message and content_type == 'text/plain':
126 message += '\n' + payload
126 message += '\n' + payload
127 except:
127 except:
128 tmpfp.close()
128 tmpfp.close()
129 os.unlink(tmpname)
129 os.unlink(tmpname)
130 raise
130 raise
131
131
132 if subject and not message.startswith(subject):
132 if subject and not message.startswith(subject):
133 message = '%s\n%s' % (subject, message)
133 message = '%s\n%s' % (subject, message)
134 tmpfp.close()
134 tmpfp.close()
135 if not diffs_seen:
135 if not diffs_seen:
136 os.unlink(tmpname)
136 os.unlink(tmpname)
137 return None, message, user, date, branch, None, None, None
137 return None, message, user, date, branch, None, None, None
138 p1 = parents and parents.pop(0) or None
138 p1 = parents and parents.pop(0) or None
139 p2 = parents and parents.pop(0) or None
139 p2 = parents and parents.pop(0) or None
140 return tmpname, message, user, date, branch, nodeid, p1, p2
140 return tmpname, message, user, date, branch, nodeid, p1, p2
141
141
142 GP_PATCH = 1 << 0 # we have to run patch
142 GP_PATCH = 1 << 0 # we have to run patch
143 GP_FILTER = 1 << 1 # there's some copy/rename operation
143 GP_FILTER = 1 << 1 # there's some copy/rename operation
144 GP_BINARY = 1 << 2 # there's a binary patch
144 GP_BINARY = 1 << 2 # there's a binary patch
145
145
146 def readgitpatch(fp, firstline=None):
146 def readgitpatch(fp, firstline=None):
147 """extract git-style metadata about patches from <patchname>"""
147 """extract git-style metadata about patches from <patchname>"""
148 class gitpatch:
148 class gitpatch:
149 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
149 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
150 def __init__(self, path):
150 def __init__(self, path):
151 self.path = path
151 self.path = path
152 self.oldpath = None
152 self.oldpath = None
153 self.mode = None
153 self.mode = None
154 self.op = 'MODIFY'
154 self.op = 'MODIFY'
155 self.lineno = 0
155 self.lineno = 0
156 self.binary = False
156 self.binary = False
157
157
158 def reader(fp, firstline):
158 def reader(fp, firstline):
159 if firstline is not None:
159 if firstline is not None:
160 yield firstline
160 yield firstline
161 for line in fp:
161 for line in fp:
162 yield line
162 yield line
163
163
164 # Filter patch for git information
164 # Filter patch for git information
165 gitre = re.compile('diff --git a/(.*) b/(.*)')
165 gitre = re.compile('diff --git a/(.*) b/(.*)')
166 gp = None
166 gp = None
167 gitpatches = []
167 gitpatches = []
168 # Can have a git patch with only metadata, causing patch to complain
168 # Can have a git patch with only metadata, causing patch to complain
169 dopatch = 0
169 dopatch = 0
170
170
171 lineno = 0
171 lineno = 0
172 for line in reader(fp, firstline):
172 for line in reader(fp, firstline):
173 lineno += 1
173 lineno += 1
174 if line.startswith('diff --git'):
174 if line.startswith('diff --git'):
175 m = gitre.match(line)
175 m = gitre.match(line)
176 if m:
176 if m:
177 if gp:
177 if gp:
178 gitpatches.append(gp)
178 gitpatches.append(gp)
179 src, dst = m.group(1, 2)
179 src, dst = m.group(1, 2)
180 gp = gitpatch(dst)
180 gp = gitpatch(dst)
181 gp.lineno = lineno
181 gp.lineno = lineno
182 elif gp:
182 elif gp:
183 if line.startswith('--- '):
183 if line.startswith('--- '):
184 if gp.op in ('COPY', 'RENAME'):
184 if gp.op in ('COPY', 'RENAME'):
185 dopatch |= GP_FILTER
185 dopatch |= GP_FILTER
186 gitpatches.append(gp)
186 gitpatches.append(gp)
187 gp = None
187 gp = None
188 dopatch |= GP_PATCH
188 dopatch |= GP_PATCH
189 continue
189 continue
190 if line.startswith('rename from '):
190 if line.startswith('rename from '):
191 gp.op = 'RENAME'
191 gp.op = 'RENAME'
192 gp.oldpath = line[12:].rstrip()
192 gp.oldpath = line[12:].rstrip()
193 elif line.startswith('rename to '):
193 elif line.startswith('rename to '):
194 gp.path = line[10:].rstrip()
194 gp.path = line[10:].rstrip()
195 elif line.startswith('copy from '):
195 elif line.startswith('copy from '):
196 gp.op = 'COPY'
196 gp.op = 'COPY'
197 gp.oldpath = line[10:].rstrip()
197 gp.oldpath = line[10:].rstrip()
198 elif line.startswith('copy to '):
198 elif line.startswith('copy to '):
199 gp.path = line[8:].rstrip()
199 gp.path = line[8:].rstrip()
200 elif line.startswith('deleted file'):
200 elif line.startswith('deleted file'):
201 gp.op = 'DELETE'
201 gp.op = 'DELETE'
202 elif line.startswith('new file mode '):
202 elif line.startswith('new file mode '):
203 gp.op = 'ADD'
203 gp.op = 'ADD'
204 gp.mode = int(line.rstrip()[-6:], 8)
204 gp.mode = int(line.rstrip()[-6:], 8)
205 elif line.startswith('new mode '):
205 elif line.startswith('new mode '):
206 gp.mode = int(line.rstrip()[-6:], 8)
206 gp.mode = int(line.rstrip()[-6:], 8)
207 elif line.startswith('GIT binary patch'):
207 elif line.startswith('GIT binary patch'):
208 dopatch |= GP_BINARY
208 dopatch |= GP_BINARY
209 gp.binary = True
209 gp.binary = True
210 if gp:
210 if gp:
211 gitpatches.append(gp)
211 gitpatches.append(gp)
212
212
213 if not gitpatches:
213 if not gitpatches:
214 dopatch = GP_PATCH
214 dopatch = GP_PATCH
215
215
216 return (dopatch, gitpatches)
216 return (dopatch, gitpatches)
217
217
218 def patch(patchname, ui, strip=1, cwd=None, files={}):
218 def patch(patchname, ui, strip=1, cwd=None, files={}):
219 """apply <patchname> to the working directory.
219 """apply <patchname> to the working directory.
220 returns whether patch was applied with fuzz factor."""
220 returns whether patch was applied with fuzz factor."""
221 patcher = ui.config('ui', 'patch')
221 patcher = ui.config('ui', 'patch')
222 args = []
222 args = []
223 try:
223 try:
224 if patcher:
224 if patcher:
225 return externalpatch(patcher, args, patchname, ui, strip, cwd,
225 return externalpatch(patcher, args, patchname, ui, strip, cwd,
226 files)
226 files)
227 else:
227 else:
228 try:
228 try:
229 return internalpatch(patchname, ui, strip, cwd, files)
229 return internalpatch(patchname, ui, strip, cwd, files)
230 except NoHunks:
230 except NoHunks:
231 patcher = util.find_exe('gpatch') or util.find_exe('patch')
231 patcher = util.find_exe('gpatch') or util.find_exe('patch')
232 ui.debug('no valid hunks found; trying with %r instead\n' %
232 ui.debug('no valid hunks found; trying with %r instead\n' %
233 patcher)
233 patcher)
234 if util.needbinarypatch():
234 if util.needbinarypatch():
235 args.append('--binary')
235 args.append('--binary')
236 return externalpatch(patcher, args, patchname, ui, strip, cwd,
236 return externalpatch(patcher, args, patchname, ui, strip, cwd,
237 files)
237 files)
238 except PatchError, err:
238 except PatchError, err:
239 s = str(err)
239 s = str(err)
240 if s:
240 if s:
241 raise util.Abort(s)
241 raise util.Abort(s)
242 else:
242 else:
243 raise util.Abort(_('patch failed to apply'))
243 raise util.Abort(_('patch failed to apply'))
244
244
245 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
245 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
246 """use <patcher> to apply <patchname> to the working directory.
246 """use <patcher> to apply <patchname> to the working directory.
247 returns whether patch was applied with fuzz factor."""
247 returns whether patch was applied with fuzz factor."""
248
248
249 fuzz = False
249 fuzz = False
250 if cwd:
250 if cwd:
251 args.append('-d %s' % util.shellquote(cwd))
251 args.append('-d %s' % util.shellquote(cwd))
252 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
252 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
253 util.shellquote(patchname)))
253 util.shellquote(patchname)))
254
254
255 for line in fp:
255 for line in fp:
256 line = line.rstrip()
256 line = line.rstrip()
257 ui.note(line + '\n')
257 ui.note(line + '\n')
258 if line.startswith('patching file '):
258 if line.startswith('patching file '):
259 pf = util.parse_patch_output(line)
259 pf = util.parse_patch_output(line)
260 printed_file = False
260 printed_file = False
261 files.setdefault(pf, (None, None))
261 files.setdefault(pf, (None, None))
262 elif line.find('with fuzz') >= 0:
262 elif line.find('with fuzz') >= 0:
263 fuzz = True
263 fuzz = True
264 if not printed_file:
264 if not printed_file:
265 ui.warn(pf + '\n')
265 ui.warn(pf + '\n')
266 printed_file = True
266 printed_file = True
267 ui.warn(line + '\n')
267 ui.warn(line + '\n')
268 elif line.find('saving rejects to file') >= 0:
268 elif line.find('saving rejects to file') >= 0:
269 ui.warn(line + '\n')
269 ui.warn(line + '\n')
270 elif line.find('FAILED') >= 0:
270 elif line.find('FAILED') >= 0:
271 if not printed_file:
271 if not printed_file:
272 ui.warn(pf + '\n')
272 ui.warn(pf + '\n')
273 printed_file = True
273 printed_file = True
274 ui.warn(line + '\n')
274 ui.warn(line + '\n')
275 code = fp.close()
275 code = fp.close()
276 if code:
276 if code:
277 raise PatchError(_("patch command failed: %s") %
277 raise PatchError(_("patch command failed: %s") %
278 util.explain_exit(code)[0])
278 util.explain_exit(code)[0])
279 return fuzz
279 return fuzz
280
280
281 def internalpatch(patchobj, ui, strip, cwd, files={}):
281 def internalpatch(patchobj, ui, strip, cwd, files={}):
282 """use builtin patch to apply <patchobj> to the working directory.
282 """use builtin patch to apply <patchobj> to the working directory.
283 returns whether patch was applied with fuzz factor."""
283 returns whether patch was applied with fuzz factor."""
284 try:
284 try:
285 fp = file(patchobj, 'rb')
285 fp = file(patchobj, 'rb')
286 except TypeError:
286 except TypeError:
287 fp = patchobj
287 fp = patchobj
288 if cwd:
288 if cwd:
289 curdir = os.getcwd()
289 curdir = os.getcwd()
290 os.chdir(cwd)
290 os.chdir(cwd)
291 try:
291 try:
292 ret = applydiff(ui, fp, files, strip=strip)
292 ret = applydiff(ui, fp, files, strip=strip)
293 finally:
293 finally:
294 if cwd:
294 if cwd:
295 os.chdir(curdir)
295 os.chdir(curdir)
296 if ret < 0:
296 if ret < 0:
297 raise PatchError
297 raise PatchError
298 return ret > 0
298 return ret > 0
299
299
300 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
300 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
301 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
301 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
302 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
302 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
303
303
304 class patchfile:
304 class patchfile:
305 def __init__(self, ui, fname):
305 def __init__(self, ui, fname):
306 self.fname = fname
306 self.fname = fname
307 self.ui = ui
307 self.ui = ui
308 try:
308 try:
309 fp = file(fname, 'rb')
309 fp = file(fname, 'rb')
310 self.lines = fp.readlines()
310 self.lines = fp.readlines()
311 self.exists = True
311 self.exists = True
312 except IOError:
312 except IOError:
313 dirname = os.path.dirname(fname)
313 dirname = os.path.dirname(fname)
314 if dirname and not os.path.isdir(dirname):
314 if dirname and not os.path.isdir(dirname):
315 dirs = dirname.split(os.path.sep)
315 dirs = dirname.split(os.path.sep)
316 d = ""
316 d = ""
317 for x in dirs:
317 for x in dirs:
318 d = os.path.join(d, x)
318 d = os.path.join(d, x)
319 if not os.path.isdir(d):
319 if not os.path.isdir(d):
320 os.mkdir(d)
320 os.mkdir(d)
321 self.lines = []
321 self.lines = []
322 self.exists = False
322 self.exists = False
323
323
324 self.hash = {}
324 self.hash = {}
325 self.dirty = 0
325 self.dirty = 0
326 self.offset = 0
326 self.offset = 0
327 self.rej = []
327 self.rej = []
328 self.fileprinted = False
328 self.fileprinted = False
329 self.printfile(False)
329 self.printfile(False)
330 self.hunks = 0
330 self.hunks = 0
331
331
332 def printfile(self, warn):
332 def printfile(self, warn):
333 if self.fileprinted:
333 if self.fileprinted:
334 return
334 return
335 if warn or self.ui.verbose:
335 if warn or self.ui.verbose:
336 self.fileprinted = True
336 self.fileprinted = True
337 s = _("patching file %s\n") % self.fname
337 s = _("patching file %s\n") % self.fname
338 if warn:
338 if warn:
339 self.ui.warn(s)
339 self.ui.warn(s)
340 else:
340 else:
341 self.ui.note(s)
341 self.ui.note(s)
342
342
343
343
344 def findlines(self, l, linenum):
344 def findlines(self, l, linenum):
345 # looks through the hash and finds candidate lines. The
345 # looks through the hash and finds candidate lines. The
346 # result is a list of line numbers sorted based on distance
346 # result is a list of line numbers sorted based on distance
347 # from linenum
347 # from linenum
348 def sorter(a, b):
348 def sorter(a, b):
349 vala = abs(a - linenum)
349 vala = abs(a - linenum)
350 valb = abs(b - linenum)
350 valb = abs(b - linenum)
351 return cmp(vala, valb)
351 return cmp(vala, valb)
352
352
353 try:
353 try:
354 cand = self.hash[l]
354 cand = self.hash[l]
355 except:
355 except:
356 return []
356 return []
357
357
358 if len(cand) > 1:
358 if len(cand) > 1:
359 # resort our list of potentials forward then back.
359 # resort our list of potentials forward then back.
360 cand.sort(sorter)
360 cand.sort(sorter)
361 return cand
361 return cand
362
362
363 def hashlines(self):
363 def hashlines(self):
364 self.hash = {}
364 self.hash = {}
365 for x in xrange(len(self.lines)):
365 for x in xrange(len(self.lines)):
366 s = self.lines[x]
366 s = self.lines[x]
367 self.hash.setdefault(s, []).append(x)
367 self.hash.setdefault(s, []).append(x)
368
368
369 def write_rej(self):
369 def write_rej(self):
370 # our rejects are a little different from patch(1). This always
370 # our rejects are a little different from patch(1). This always
371 # creates rejects in the same form as the original patch. A file
371 # creates rejects in the same form as the original patch. A file
372 # header is inserted so that you can run the reject through patch again
372 # header is inserted so that you can run the reject through patch again
373 # without having to type the filename.
373 # without having to type the filename.
374
374
375 if not self.rej:
375 if not self.rej:
376 return
376 return
377 if self.hunks != 1:
377 if self.hunks != 1:
378 hunkstr = "s"
378 hunkstr = "s"
379 else:
379 else:
380 hunkstr = ""
380 hunkstr = ""
381
381
382 fname = self.fname + ".rej"
382 fname = self.fname + ".rej"
383 self.ui.warn(
383 self.ui.warn(
384 _("%d out of %d hunk%s FAILED -- saving rejects to file %s\n") %
384 _("%d out of %d hunk%s FAILED -- saving rejects to file %s\n") %
385 (len(self.rej), self.hunks, hunkstr, fname))
385 (len(self.rej), self.hunks, hunkstr, fname))
386 try: os.unlink(fname)
386 try: os.unlink(fname)
387 except:
387 except:
388 pass
388 pass
389 fp = file(fname, 'wb')
389 fp = file(fname, 'wb')
390 base = os.path.basename(self.fname)
390 base = os.path.basename(self.fname)
391 fp.write("--- %s\n+++ %s\n" % (base, base))
391 fp.write("--- %s\n+++ %s\n" % (base, base))
392 for x in self.rej:
392 for x in self.rej:
393 for l in x.hunk:
393 for l in x.hunk:
394 fp.write(l)
394 fp.write(l)
395 if l[-1] != '\n':
395 if l[-1] != '\n':
396 fp.write("\n\ No newline at end of file\n")
396 fp.write("\n\ No newline at end of file\n")
397
397
398 def write(self, dest=None):
398 def write(self, dest=None):
399 if self.dirty:
399 if self.dirty:
400 if not dest:
400 if not dest:
401 dest = self.fname
401 dest = self.fname
402 st = None
402 st = None
403 try:
403 try:
404 st = os.lstat(dest)
404 st = os.lstat(dest)
405 except OSError, inst:
405 except OSError, inst:
406 if inst.errno != errno.ENOENT:
406 if inst.errno != errno.ENOENT:
407 raise
407 raise
408 if st and st.st_nlink > 1:
408 if st and st.st_nlink > 1:
409 os.unlink(dest)
409 os.unlink(dest)
410 fp = file(dest, 'wb')
410 fp = file(dest, 'wb')
411 if st and st.st_nlink > 1:
411 if st and st.st_nlink > 1:
412 os.chmod(dest, st.st_mode)
412 os.chmod(dest, st.st_mode)
413 fp.writelines(self.lines)
413 fp.writelines(self.lines)
414 fp.close()
414 fp.close()
415
415
416 def close(self):
416 def close(self):
417 self.write()
417 self.write()
418 self.write_rej()
418 self.write_rej()
419
419
420 def apply(self, h, reverse):
420 def apply(self, h, reverse):
421 if not h.complete():
421 if not h.complete():
422 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
422 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
423 (h.number, h.desc, len(h.a), h.lena, len(h.b),
423 (h.number, h.desc, len(h.a), h.lena, len(h.b),
424 h.lenb))
424 h.lenb))
425
425
426 self.hunks += 1
426 self.hunks += 1
427 if reverse:
427 if reverse:
428 h.reverse()
428 h.reverse()
429
429
430 if self.exists and h.createfile():
430 if self.exists and h.createfile():
431 self.ui.warn(_("file %s already exists\n") % self.fname)
431 self.ui.warn(_("file %s already exists\n") % self.fname)
432 self.rej.append(h)
432 self.rej.append(h)
433 return -1
433 return -1
434
434
435 if isinstance(h, binhunk):
435 if isinstance(h, binhunk):
436 if h.rmfile():
436 if h.rmfile():
437 os.unlink(self.fname)
437 os.unlink(self.fname)
438 else:
438 else:
439 self.lines[:] = h.new()
439 self.lines[:] = h.new()
440 self.offset += len(h.new())
440 self.offset += len(h.new())
441 self.dirty = 1
441 self.dirty = 1
442 return 0
442 return 0
443
443
444 # fast case first, no offsets, no fuzz
444 # fast case first, no offsets, no fuzz
445 old = h.old()
445 old = h.old()
446 # patch starts counting at 1 unless we are adding the file
446 # patch starts counting at 1 unless we are adding the file
447 if h.starta == 0:
447 if h.starta == 0:
448 start = 0
448 start = 0
449 else:
449 else:
450 start = h.starta + self.offset - 1
450 start = h.starta + self.offset - 1
451 orig_start = start
451 orig_start = start
452 if diffhelpers.testhunk(old, self.lines, start) == 0:
452 if diffhelpers.testhunk(old, self.lines, start) == 0:
453 if h.rmfile():
453 if h.rmfile():
454 os.unlink(self.fname)
454 os.unlink(self.fname)
455 else:
455 else:
456 self.lines[start : start + h.lena] = h.new()
456 self.lines[start : start + h.lena] = h.new()
457 self.offset += h.lenb - h.lena
457 self.offset += h.lenb - h.lena
458 self.dirty = 1
458 self.dirty = 1
459 return 0
459 return 0
460
460
461 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
461 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
462 self.hashlines()
462 self.hashlines()
463 if h.hunk[-1][0] != ' ':
463 if h.hunk[-1][0] != ' ':
464 # if the hunk tried to put something at the bottom of the file
464 # if the hunk tried to put something at the bottom of the file
465 # override the start line and use eof here
465 # override the start line and use eof here
466 search_start = len(self.lines)
466 search_start = len(self.lines)
467 else:
467 else:
468 search_start = orig_start
468 search_start = orig_start
469
469
470 for fuzzlen in xrange(3):
470 for fuzzlen in xrange(3):
471 for toponly in [ True, False ]:
471 for toponly in [ True, False ]:
472 old = h.old(fuzzlen, toponly)
472 old = h.old(fuzzlen, toponly)
473
473
474 cand = self.findlines(old[0][1:], search_start)
474 cand = self.findlines(old[0][1:], search_start)
475 for l in cand:
475 for l in cand:
476 if diffhelpers.testhunk(old, self.lines, l) == 0:
476 if diffhelpers.testhunk(old, self.lines, l) == 0:
477 newlines = h.new(fuzzlen, toponly)
477 newlines = h.new(fuzzlen, toponly)
478 self.lines[l : l + len(old)] = newlines
478 self.lines[l : l + len(old)] = newlines
479 self.offset += len(newlines) - len(old)
479 self.offset += len(newlines) - len(old)
480 self.dirty = 1
480 self.dirty = 1
481 if fuzzlen:
481 if fuzzlen:
482 fuzzstr = "with fuzz %d " % fuzzlen
482 fuzzstr = "with fuzz %d " % fuzzlen
483 f = self.ui.warn
483 f = self.ui.warn
484 self.printfile(True)
484 self.printfile(True)
485 else:
485 else:
486 fuzzstr = ""
486 fuzzstr = ""
487 f = self.ui.note
487 f = self.ui.note
488 offset = l - orig_start - fuzzlen
488 offset = l - orig_start - fuzzlen
489 if offset == 1:
489 if offset == 1:
490 linestr = "line"
490 linestr = "line"
491 else:
491 else:
492 linestr = "lines"
492 linestr = "lines"
493 f(_("Hunk #%d succeeded at %d %s(offset %d %s).\n") %
493 f(_("Hunk #%d succeeded at %d %s(offset %d %s).\n") %
494 (h.number, l+1, fuzzstr, offset, linestr))
494 (h.number, l+1, fuzzstr, offset, linestr))
495 return fuzzlen
495 return fuzzlen
496 self.printfile(True)
496 self.printfile(True)
497 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
497 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
498 self.rej.append(h)
498 self.rej.append(h)
499 return -1
499 return -1
500
500
501 class hunk:
501 class hunk:
502 def __init__(self, desc, num, lr, context):
502 def __init__(self, desc, num, lr, context):
503 self.number = num
503 self.number = num
504 self.desc = desc
504 self.desc = desc
505 self.hunk = [ desc ]
505 self.hunk = [ desc ]
506 self.a = []
506 self.a = []
507 self.b = []
507 self.b = []
508 if context:
508 if context:
509 self.read_context_hunk(lr)
509 self.read_context_hunk(lr)
510 else:
510 else:
511 self.read_unified_hunk(lr)
511 self.read_unified_hunk(lr)
512
512
513 def read_unified_hunk(self, lr):
513 def read_unified_hunk(self, lr):
514 m = unidesc.match(self.desc)
514 m = unidesc.match(self.desc)
515 if not m:
515 if not m:
516 raise PatchError(_("bad hunk #%d") % self.number)
516 raise PatchError(_("bad hunk #%d") % self.number)
517 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
517 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
518 if self.lena == None:
518 if self.lena == None:
519 self.lena = 1
519 self.lena = 1
520 else:
520 else:
521 self.lena = int(self.lena)
521 self.lena = int(self.lena)
522 if self.lenb == None:
522 if self.lenb == None:
523 self.lenb = 1
523 self.lenb = 1
524 else:
524 else:
525 self.lenb = int(self.lenb)
525 self.lenb = int(self.lenb)
526 self.starta = int(self.starta)
526 self.starta = int(self.starta)
527 self.startb = int(self.startb)
527 self.startb = int(self.startb)
528 diffhelpers.addlines(lr.fp, self.hunk, self.lena, self.lenb, self.a, self.b)
528 diffhelpers.addlines(lr.fp, self.hunk, self.lena, self.lenb, self.a, self.b)
529 # if we hit eof before finishing out the hunk, the last line will
529 # if we hit eof before finishing out the hunk, the last line will
530 # be zero length. Lets try to fix it up.
530 # be zero length. Lets try to fix it up.
531 while len(self.hunk[-1]) == 0:
531 while len(self.hunk[-1]) == 0:
532 del self.hunk[-1]
532 del self.hunk[-1]
533 del self.a[-1]
533 del self.a[-1]
534 del self.b[-1]
534 del self.b[-1]
535 self.lena -= 1
535 self.lena -= 1
536 self.lenb -= 1
536 self.lenb -= 1
537
537
538 def read_context_hunk(self, lr):
538 def read_context_hunk(self, lr):
539 self.desc = lr.readline()
539 self.desc = lr.readline()
540 m = contextdesc.match(self.desc)
540 m = contextdesc.match(self.desc)
541 if not m:
541 if not m:
542 raise PatchError(_("bad hunk #%d") % self.number)
542 raise PatchError(_("bad hunk #%d") % self.number)
543 foo, self.starta, foo2, aend, foo3 = m.groups()
543 foo, self.starta, foo2, aend, foo3 = m.groups()
544 self.starta = int(self.starta)
544 self.starta = int(self.starta)
545 if aend == None:
545 if aend == None:
546 aend = self.starta
546 aend = self.starta
547 self.lena = int(aend) - self.starta
547 self.lena = int(aend) - self.starta
548 if self.starta:
548 if self.starta:
549 self.lena += 1
549 self.lena += 1
550 for x in xrange(self.lena):
550 for x in xrange(self.lena):
551 l = lr.readline()
551 l = lr.readline()
552 if l.startswith('---'):
552 if l.startswith('---'):
553 lr.push(l)
553 lr.push(l)
554 break
554 break
555 s = l[2:]
555 s = l[2:]
556 if l.startswith('- ') or l.startswith('! '):
556 if l.startswith('- ') or l.startswith('! '):
557 u = '-' + s
557 u = '-' + s
558 elif l.startswith(' '):
558 elif l.startswith(' '):
559 u = ' ' + s
559 u = ' ' + s
560 else:
560 else:
561 raise PatchError(_("bad hunk #%d old text line %d") %
561 raise PatchError(_("bad hunk #%d old text line %d") %
562 (self.number, x))
562 (self.number, x))
563 self.a.append(u)
563 self.a.append(u)
564 self.hunk.append(u)
564 self.hunk.append(u)
565
565
566 l = lr.readline()
566 l = lr.readline()
567 if l.startswith('\ '):
567 if l.startswith('\ '):
568 s = self.a[-1][:-1]
568 s = self.a[-1][:-1]
569 self.a[-1] = s
569 self.a[-1] = s
570 self.hunk[-1] = s
570 self.hunk[-1] = s
571 l = lr.readline()
571 l = lr.readline()
572 m = contextdesc.match(l)
572 m = contextdesc.match(l)
573 if not m:
573 if not m:
574 raise PatchError(_("bad hunk #%d") % self.number)
574 raise PatchError(_("bad hunk #%d") % self.number)
575 foo, self.startb, foo2, bend, foo3 = m.groups()
575 foo, self.startb, foo2, bend, foo3 = m.groups()
576 self.startb = int(self.startb)
576 self.startb = int(self.startb)
577 if bend == None:
577 if bend == None:
578 bend = self.startb
578 bend = self.startb
579 self.lenb = int(bend) - self.startb
579 self.lenb = int(bend) - self.startb
580 if self.startb:
580 if self.startb:
581 self.lenb += 1
581 self.lenb += 1
582 hunki = 1
582 hunki = 1
583 for x in xrange(self.lenb):
583 for x in xrange(self.lenb):
584 l = lr.readline()
584 l = lr.readline()
585 if l.startswith('\ '):
585 if l.startswith('\ '):
586 s = self.b[-1][:-1]
586 s = self.b[-1][:-1]
587 self.b[-1] = s
587 self.b[-1] = s
588 self.hunk[hunki-1] = s
588 self.hunk[hunki-1] = s
589 continue
589 continue
590 if not l:
590 if not l:
591 lr.push(l)
591 lr.push(l)
592 break
592 break
593 s = l[2:]
593 s = l[2:]
594 if l.startswith('+ ') or l.startswith('! '):
594 if l.startswith('+ ') or l.startswith('! '):
595 u = '+' + s
595 u = '+' + s
596 elif l.startswith(' '):
596 elif l.startswith(' '):
597 u = ' ' + s
597 u = ' ' + s
598 elif len(self.b) == 0:
598 elif len(self.b) == 0:
599 # this can happen when the hunk does not add any lines
599 # this can happen when the hunk does not add any lines
600 lr.push(l)
600 lr.push(l)
601 break
601 break
602 else:
602 else:
603 raise PatchError(_("bad hunk #%d old text line %d") %
603 raise PatchError(_("bad hunk #%d old text line %d") %
604 (self.number, x))
604 (self.number, x))
605 self.b.append(s)
605 self.b.append(s)
606 while True:
606 while True:
607 if hunki >= len(self.hunk):
607 if hunki >= len(self.hunk):
608 h = ""
608 h = ""
609 else:
609 else:
610 h = self.hunk[hunki]
610 h = self.hunk[hunki]
611 hunki += 1
611 hunki += 1
612 if h == u:
612 if h == u:
613 break
613 break
614 elif h.startswith('-'):
614 elif h.startswith('-'):
615 continue
615 continue
616 else:
616 else:
617 self.hunk.insert(hunki-1, u)
617 self.hunk.insert(hunki-1, u)
618 break
618 break
619
619
620 if not self.a:
620 if not self.a:
621 # this happens when lines were only added to the hunk
621 # this happens when lines were only added to the hunk
622 for x in self.hunk:
622 for x in self.hunk:
623 if x.startswith('-') or x.startswith(' '):
623 if x.startswith('-') or x.startswith(' '):
624 self.a.append(x)
624 self.a.append(x)
625 if not self.b:
625 if not self.b:
626 # this happens when lines were only deleted from the hunk
626 # this happens when lines were only deleted from the hunk
627 for x in self.hunk:
627 for x in self.hunk:
628 if x.startswith('+') or x.startswith(' '):
628 if x.startswith('+') or x.startswith(' '):
629 self.b.append(x[1:])
629 self.b.append(x[1:])
630 # @@ -start,len +start,len @@
630 # @@ -start,len +start,len @@
631 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
631 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
632 self.startb, self.lenb)
632 self.startb, self.lenb)
633 self.hunk[0] = self.desc
633 self.hunk[0] = self.desc
634
634
635 def reverse(self):
635 def reverse(self):
636 origlena = self.lena
636 origlena = self.lena
637 origstarta = self.starta
637 origstarta = self.starta
638 self.lena = self.lenb
638 self.lena = self.lenb
639 self.starta = self.startb
639 self.starta = self.startb
640 self.lenb = origlena
640 self.lenb = origlena
641 self.startb = origstarta
641 self.startb = origstarta
642 self.a = []
642 self.a = []
643 self.b = []
643 self.b = []
644 # self.hunk[0] is the @@ description
644 # self.hunk[0] is the @@ description
645 for x in xrange(1, len(self.hunk)):
645 for x in xrange(1, len(self.hunk)):
646 o = self.hunk[x]
646 o = self.hunk[x]
647 if o.startswith('-'):
647 if o.startswith('-'):
648 n = '+' + o[1:]
648 n = '+' + o[1:]
649 self.b.append(o[1:])
649 self.b.append(o[1:])
650 elif o.startswith('+'):
650 elif o.startswith('+'):
651 n = '-' + o[1:]
651 n = '-' + o[1:]
652 self.a.append(n)
652 self.a.append(n)
653 else:
653 else:
654 n = o
654 n = o
655 self.b.append(o[1:])
655 self.b.append(o[1:])
656 self.a.append(o)
656 self.a.append(o)
657 self.hunk[x] = o
657 self.hunk[x] = o
658
658
659 def fix_newline(self):
659 def fix_newline(self):
660 diffhelpers.fix_newline(self.hunk, self.a, self.b)
660 diffhelpers.fix_newline(self.hunk, self.a, self.b)
661
661
662 def complete(self):
662 def complete(self):
663 return len(self.a) == self.lena and len(self.b) == self.lenb
663 return len(self.a) == self.lena and len(self.b) == self.lenb
664
664
665 def createfile(self):
665 def createfile(self):
666 return self.starta == 0 and self.lena == 0
666 return self.starta == 0 and self.lena == 0
667
667
668 def rmfile(self):
668 def rmfile(self):
669 return self.startb == 0 and self.lenb == 0
669 return self.startb == 0 and self.lenb == 0
670
670
671 def fuzzit(self, l, fuzz, toponly):
671 def fuzzit(self, l, fuzz, toponly):
672 # this removes context lines from the top and bottom of list 'l'. It
672 # this removes context lines from the top and bottom of list 'l'. It
673 # checks the hunk to make sure only context lines are removed, and then
673 # checks the hunk to make sure only context lines are removed, and then
674 # returns a new shortened list of lines.
674 # returns a new shortened list of lines.
675 fuzz = min(fuzz, len(l)-1)
675 fuzz = min(fuzz, len(l)-1)
676 if fuzz:
676 if fuzz:
677 top = 0
677 top = 0
678 bot = 0
678 bot = 0
679 hlen = len(self.hunk)
679 hlen = len(self.hunk)
680 for x in xrange(hlen-1):
680 for x in xrange(hlen-1):
681 # the hunk starts with the @@ line, so use x+1
681 # the hunk starts with the @@ line, so use x+1
682 if self.hunk[x+1][0] == ' ':
682 if self.hunk[x+1][0] == ' ':
683 top += 1
683 top += 1
684 else:
684 else:
685 break
685 break
686 if not toponly:
686 if not toponly:
687 for x in xrange(hlen-1):
687 for x in xrange(hlen-1):
688 if self.hunk[hlen-bot-1][0] == ' ':
688 if self.hunk[hlen-bot-1][0] == ' ':
689 bot += 1
689 bot += 1
690 else:
690 else:
691 break
691 break
692
692
693 # top and bot now count context in the hunk
693 # top and bot now count context in the hunk
694 # adjust them if either one is short
694 # adjust them if either one is short
695 context = max(top, bot, 3)
695 context = max(top, bot, 3)
696 if bot < context:
696 if bot < context:
697 bot = max(0, fuzz - (context - bot))
697 bot = max(0, fuzz - (context - bot))
698 else:
698 else:
699 bot = min(fuzz, bot)
699 bot = min(fuzz, bot)
700 if top < context:
700 if top < context:
701 top = max(0, fuzz - (context - top))
701 top = max(0, fuzz - (context - top))
702 else:
702 else:
703 top = min(fuzz, top)
703 top = min(fuzz, top)
704
704
705 return l[top:len(l)-bot]
705 return l[top:len(l)-bot]
706 return l
706 return l
707
707
708 def old(self, fuzz=0, toponly=False):
708 def old(self, fuzz=0, toponly=False):
709 return self.fuzzit(self.a, fuzz, toponly)
709 return self.fuzzit(self.a, fuzz, toponly)
710
710
711 def newctrl(self):
711 def newctrl(self):
712 res = []
712 res = []
713 for x in self.hunk:
713 for x in self.hunk:
714 c = x[0]
714 c = x[0]
715 if c == ' ' or c == '+':
715 if c == ' ' or c == '+':
716 res.append(x)
716 res.append(x)
717 return res
717 return res
718
718
719 def new(self, fuzz=0, toponly=False):
719 def new(self, fuzz=0, toponly=False):
720 return self.fuzzit(self.b, fuzz, toponly)
720 return self.fuzzit(self.b, fuzz, toponly)
721
721
722 class binhunk:
722 class binhunk:
723 'A binary patch file. Only understands literals so far.'
723 'A binary patch file. Only understands literals so far.'
724 def __init__(self, gitpatch):
724 def __init__(self, gitpatch):
725 self.gitpatch = gitpatch
725 self.gitpatch = gitpatch
726 self.text = None
726 self.text = None
727 self.hunk = ['GIT binary patch\n']
727 self.hunk = ['GIT binary patch\n']
728
728
729 def createfile(self):
729 def createfile(self):
730 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
730 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
731
731
732 def rmfile(self):
732 def rmfile(self):
733 return self.gitpatch.op == 'DELETE'
733 return self.gitpatch.op == 'DELETE'
734
734
735 def complete(self):
735 def complete(self):
736 return self.text is not None
736 return self.text is not None
737
737
738 def new(self):
738 def new(self):
739 return [self.text]
739 return [self.text]
740
740
741 def extract(self, fp):
741 def extract(self, fp):
742 line = fp.readline()
742 line = fp.readline()
743 self.hunk.append(line)
743 self.hunk.append(line)
744 while line and not line.startswith('literal '):
744 while line and not line.startswith('literal '):
745 line = fp.readline()
745 line = fp.readline()
746 self.hunk.append(line)
746 self.hunk.append(line)
747 if not line:
747 if not line:
748 raise PatchError(_('could not extract binary patch'))
748 raise PatchError(_('could not extract binary patch'))
749 size = int(line[8:].rstrip())
749 size = int(line[8:].rstrip())
750 dec = []
750 dec = []
751 line = fp.readline()
751 line = fp.readline()
752 self.hunk.append(line)
752 self.hunk.append(line)
753 while len(line) > 1:
753 while len(line) > 1:
754 l = line[0]
754 l = line[0]
755 if l <= 'Z' and l >= 'A':
755 if l <= 'Z' and l >= 'A':
756 l = ord(l) - ord('A') + 1
756 l = ord(l) - ord('A') + 1
757 else:
757 else:
758 l = ord(l) - ord('a') + 27
758 l = ord(l) - ord('a') + 27
759 dec.append(base85.b85decode(line[1:-1])[:l])
759 dec.append(base85.b85decode(line[1:-1])[:l])
760 line = fp.readline()
760 line = fp.readline()
761 self.hunk.append(line)
761 self.hunk.append(line)
762 text = zlib.decompress(''.join(dec))
762 text = zlib.decompress(''.join(dec))
763 if len(text) != size:
763 if len(text) != size:
764 raise PatchError(_('binary patch is %d bytes, not %d') %
764 raise PatchError(_('binary patch is %d bytes, not %d') %
765 len(text), size)
765 len(text), size)
766 self.text = text
766 self.text = text
767
767
768 def parsefilename(str):
768 def parsefilename(str):
769 # --- filename \t|space stuff
769 # --- filename \t|space stuff
770 s = str[4:]
770 s = str[4:]
771 i = s.find('\t')
771 i = s.find('\t')
772 if i < 0:
772 if i < 0:
773 i = s.find(' ')
773 i = s.find(' ')
774 if i < 0:
774 if i < 0:
775 return s
775 return s
776 return s[:i]
776 return s[:i]
777
777
778 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse):
778 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse):
779 def pathstrip(path, count=1):
779 def pathstrip(path, count=1):
780 pathlen = len(path)
780 pathlen = len(path)
781 i = 0
781 i = 0
782 if count == 0:
782 if count == 0:
783 return path.rstrip()
783 return path.rstrip()
784 while count > 0:
784 while count > 0:
785 i = path.find('/', i)
785 i = path.find('/', i)
786 if i == -1:
786 if i == -1:
787 raise PatchError(_("unable to strip away %d dirs from %s") %
787 raise PatchError(_("unable to strip away %d dirs from %s") %
788 (count, path))
788 (count, path))
789 i += 1
789 i += 1
790 # consume '//' in the path
790 # consume '//' in the path
791 while i < pathlen - 1 and path[i] == '/':
791 while i < pathlen - 1 and path[i] == '/':
792 i += 1
792 i += 1
793 count -= 1
793 count -= 1
794 return path[i:].rstrip()
794 return path[i:].rstrip()
795
795
796 nulla = afile_orig == "/dev/null"
796 nulla = afile_orig == "/dev/null"
797 nullb = bfile_orig == "/dev/null"
797 nullb = bfile_orig == "/dev/null"
798 afile = pathstrip(afile_orig, strip)
798 afile = pathstrip(afile_orig, strip)
799 gooda = os.path.exists(afile) and not nulla
799 gooda = os.path.exists(afile) and not nulla
800 bfile = pathstrip(bfile_orig, strip)
800 bfile = pathstrip(bfile_orig, strip)
801 if afile == bfile:
801 if afile == bfile:
802 goodb = gooda
802 goodb = gooda
803 else:
803 else:
804 goodb = os.path.exists(bfile) and not nullb
804 goodb = os.path.exists(bfile) and not nullb
805 createfunc = hunk.createfile
805 createfunc = hunk.createfile
806 if reverse:
806 if reverse:
807 createfunc = hunk.rmfile
807 createfunc = hunk.rmfile
808 if not goodb and not gooda and not createfunc():
808 if not goodb and not gooda and not createfunc():
809 raise PatchError(_("unable to find %s or %s for patching") %
809 raise PatchError(_("unable to find %s or %s for patching") %
810 (afile, bfile))
810 (afile, bfile))
811 if gooda and goodb:
811 if gooda and goodb:
812 fname = bfile
812 fname = bfile
813 if afile in bfile:
813 if afile in bfile:
814 fname = afile
814 fname = afile
815 elif gooda:
815 elif gooda:
816 fname = afile
816 fname = afile
817 elif not nullb:
817 elif not nullb:
818 fname = bfile
818 fname = bfile
819 if afile in bfile:
819 if afile in bfile:
820 fname = afile
820 fname = afile
821 elif not nulla:
821 elif not nulla:
822 fname = afile
822 fname = afile
823 return fname
823 return fname
824
824
825 class linereader:
825 class linereader:
826 # simple class to allow pushing lines back into the input stream
826 # simple class to allow pushing lines back into the input stream
827 def __init__(self, fp):
827 def __init__(self, fp):
828 self.fp = fp
828 self.fp = fp
829 self.buf = []
829 self.buf = []
830
830
831 def push(self, line):
831 def push(self, line):
832 self.buf.append(line)
832 self.buf.append(line)
833
833
834 def readline(self):
834 def readline(self):
835 if self.buf:
835 if self.buf:
836 l = self.buf[0]
836 l = self.buf[0]
837 del self.buf[0]
837 del self.buf[0]
838 return l
838 return l
839 return self.fp.readline()
839 return self.fp.readline()
840
840
841 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
841 def iterhunks(ui, fp, sourcefile=None):
842 rejmerge=None, updatedir=None):
842 """Read a patch and yield the following events:
843 """reads a patch from fp and tries to apply it. The dict 'changed' is
843 - ("file", afile, bfile, firsthunk): select a new target file.
844 filled in with all of the filenames changed by the patch. Returns 0
844 - ("hunk", hunk): a new hunk is ready to be applied, follows a
845 for a clean patch, -1 if any rejects were found and 1 if there was
845 "file" event.
846 any fuzz."""
846 - ("git", gitchanges): current diff is in git format, gitchanges
847 maps filenames to gitpatch records. Unique event.
848 """
847
849
848 def scangitpatch(fp, firstline, cwd=None):
850 def scangitpatch(fp, firstline):
849 '''git patches can modify a file, then copy that file to
851 '''git patches can modify a file, then copy that file to
850 a new file, but expect the source to be the unmodified form.
852 a new file, but expect the source to be the unmodified form.
851 So we scan the patch looking for that case so we can do
853 So we scan the patch looking for that case so we can do
852 the copies ahead of time.'''
854 the copies ahead of time.'''
853
855
854 pos = 0
856 pos = 0
855 try:
857 try:
856 pos = fp.tell()
858 pos = fp.tell()
857 except IOError:
859 except IOError:
858 fp = cStringIO.StringIO(fp.read())
860 fp = cStringIO.StringIO(fp.read())
859
861
860 (dopatch, gitpatches) = readgitpatch(fp, firstline)
862 (dopatch, gitpatches) = readgitpatch(fp, firstline)
861 for gp in gitpatches:
862 if gp.op in ('COPY', 'RENAME'):
863 copyfile(gp.oldpath, gp.path, basedir=cwd)
864
865 fp.seek(pos)
863 fp.seek(pos)
866
864
867 return fp, dopatch, gitpatches
865 return fp, dopatch, gitpatches
868
866
867 changed = {}
869 current_hunk = None
868 current_hunk = None
870 current_file = None
871 afile = ""
869 afile = ""
872 bfile = ""
870 bfile = ""
873 state = None
871 state = None
874 hunknum = 0
872 hunknum = 0
875 rejects = 0
873 emitfile = False
876
874
877 git = False
875 git = False
878 gitre = re.compile('diff --git (a/.*) (b/.*)')
876 gitre = re.compile('diff --git (a/.*) (b/.*)')
879
877
880 # our states
878 # our states
881 BFILE = 1
879 BFILE = 1
882 err = 0
883 context = None
880 context = None
884 lr = linereader(fp)
881 lr = linereader(fp)
885 dopatch = True
882 dopatch = True
886 gitworkdone = False
883 gitworkdone = False
887
884
888 def getpatchfile(afile, bfile, hunk):
889 try:
890 if sourcefile:
891 targetfile = patchfile(ui, sourcefile)
892 else:
893 targetfile = selectfile(afile, bfile, hunk,
894 strip, reverse)
895 targetfile = patchfile(ui, targetfile)
896 return targetfile
897 except PatchError, err:
898 ui.warn(str(err) + '\n')
899 return None
900
901 while True:
885 while True:
902 newfile = False
886 newfile = False
903 x = lr.readline()
887 x = lr.readline()
904 if not x:
888 if not x:
905 break
889 break
906 if current_hunk:
890 if current_hunk:
907 if x.startswith('\ '):
891 if x.startswith('\ '):
908 current_hunk.fix_newline()
892 current_hunk.fix_newline()
909 ret = current_file.apply(current_hunk, reverse)
893 yield 'hunk', current_hunk
910 if ret >= 0:
911 changed.setdefault(current_file.fname, (None, None))
912 if ret > 0:
913 err = 1
914 current_hunk = None
894 current_hunk = None
915 gitworkdone = False
895 gitworkdone = False
916 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
896 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
917 ((context or context == None) and x.startswith('***************')))):
897 ((context or context == None) and x.startswith('***************')))):
918 try:
898 try:
919 if context == None and x.startswith('***************'):
899 if context == None and x.startswith('***************'):
920 context = True
900 context = True
921 current_hunk = hunk(x, hunknum + 1, lr, context)
901 current_hunk = hunk(x, hunknum + 1, lr, context)
922 except PatchError, err:
902 except PatchError, err:
923 ui.debug(err)
903 ui.debug(err)
924 current_hunk = None
904 current_hunk = None
925 continue
905 continue
926 hunknum += 1
906 hunknum += 1
927 if not current_file:
907 if emitfile:
928 current_file = getpatchfile(afile, bfile, current_hunk)
908 emitfile = False
929 if not current_file:
909 yield 'file', (afile, bfile, current_hunk)
930 current_file, current_hunk = None, None
931 rejects += 1
932 continue
933 elif state == BFILE and x.startswith('GIT binary patch'):
910 elif state == BFILE and x.startswith('GIT binary patch'):
934 current_hunk = binhunk(changed[bfile[2:]][1])
911 current_hunk = binhunk(changed[bfile[2:]][1])
935 hunknum += 1
912 hunknum += 1
936 if not current_file:
913 if emitfile:
937 current_file = getpatchfile(afile, bfile, current_hunk)
914 emitfile = False
938 if not current_file:
915 yield 'file', (afile, bfile, current_hunk)
939 current_file, current_hunk = None, None
940 rejects += 1
941 continue
942 current_hunk.extract(fp)
916 current_hunk.extract(fp)
943 elif x.startswith('diff --git'):
917 elif x.startswith('diff --git'):
944 # check for git diff, scanning the whole patch file if needed
918 # check for git diff, scanning the whole patch file if needed
945 m = gitre.match(x)
919 m = gitre.match(x)
946 if m:
920 if m:
947 afile, bfile = m.group(1, 2)
921 afile, bfile = m.group(1, 2)
948 if not git:
922 if not git:
949 git = True
923 git = True
950 fp, dopatch, gitpatches = scangitpatch(fp, x)
924 fp, dopatch, gitpatches = scangitpatch(fp, x)
925 yield 'git', gitpatches
951 for gp in gitpatches:
926 for gp in gitpatches:
952 changed[gp.path] = (gp.op, gp)
927 changed[gp.path] = (gp.op, gp)
953 # else error?
928 # else error?
954 # copy/rename + modify should modify target, not source
929 # copy/rename + modify should modify target, not source
955 if changed.get(bfile[2:], (None, None))[0] in ('COPY',
930 if changed.get(bfile[2:], (None, None))[0] in ('COPY',
956 'RENAME'):
931 'RENAME'):
957 afile = bfile
932 afile = bfile
958 gitworkdone = True
933 gitworkdone = True
959 newfile = True
934 newfile = True
960 elif x.startswith('---'):
935 elif x.startswith('---'):
961 # check for a unified diff
936 # check for a unified diff
962 l2 = lr.readline()
937 l2 = lr.readline()
963 if not l2.startswith('+++'):
938 if not l2.startswith('+++'):
964 lr.push(l2)
939 lr.push(l2)
965 continue
940 continue
966 newfile = True
941 newfile = True
967 context = False
942 context = False
968 afile = parsefilename(x)
943 afile = parsefilename(x)
969 bfile = parsefilename(l2)
944 bfile = parsefilename(l2)
970 elif x.startswith('***'):
945 elif x.startswith('***'):
971 # check for a context diff
946 # check for a context diff
972 l2 = lr.readline()
947 l2 = lr.readline()
973 if not l2.startswith('---'):
948 if not l2.startswith('---'):
974 lr.push(l2)
949 lr.push(l2)
975 continue
950 continue
976 l3 = lr.readline()
951 l3 = lr.readline()
977 lr.push(l3)
952 lr.push(l3)
978 if not l3.startswith("***************"):
953 if not l3.startswith("***************"):
979 lr.push(l2)
954 lr.push(l2)
980 continue
955 continue
981 newfile = True
956 newfile = True
982 context = True
957 context = True
983 afile = parsefilename(x)
958 afile = parsefilename(x)
984 bfile = parsefilename(l2)
959 bfile = parsefilename(l2)
985
960
986 if newfile:
961 if newfile:
987 if current_file:
962 emitfile = True
988 current_file.close()
989 if rejmerge:
990 rejmerge(current_file)
991 rejects += len(current_file.rej)
992 state = BFILE
963 state = BFILE
993 current_file = None
994 hunknum = 0
964 hunknum = 0
995 if current_hunk:
965 if current_hunk:
996 if current_hunk.complete():
966 if current_hunk.complete():
967 yield 'hunk', current_hunk
968 else:
969 raise PatchError(_("malformed patch %s %s") % (afile,
970 current_hunk.desc))
971
972 if hunknum == 0 and dopatch and not gitworkdone:
973 raise NoHunks
974
975 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
976 rejmerge=None, updatedir=None):
977 """reads a patch from fp and tries to apply it. The dict 'changed' is
978 filled in with all of the filenames changed by the patch. Returns 0
979 for a clean patch, -1 if any rejects were found and 1 if there was
980 any fuzz."""
981
982 rejects = 0
983 err = 0
984 current_file = None
985 gitpatches = None
986
987 def closefile():
988 if not current_file:
989 return 0
990 current_file.close()
991 if rejmerge:
992 rejmerge(current_file)
993 return len(current_file.rej)
994
995 for state, values in iterhunks(ui, fp, sourcefile):
996 if state == 'hunk':
997 if not current_file:
998 continue
999 current_hunk = values
997 ret = current_file.apply(current_hunk, reverse)
1000 ret = current_file.apply(current_hunk, reverse)
998 if ret >= 0:
1001 if ret >= 0:
999 changed.setdefault(current_file.fname, (None, None))
1002 changed.setdefault(current_file.fname, (None, None))
1000 if ret > 0:
1003 if ret > 0:
1001 err = 1
1004 err = 1
1005 elif state == 'file':
1006 rejects += closefile()
1007 afile, bfile, first_hunk = values
1008 try:
1009 if sourcefile:
1010 current_file = patchfile(ui, sourcefile)
1011 else:
1012 current_file = selectfile(afile, bfile, first_hunk,
1013 strip, reverse)
1014 current_file = patchfile(ui, current_file)
1015 except PatchError, err:
1016 ui.warn(str(err) + '\n')
1017 current_file, current_hunk = None, None
1018 rejects += 1
1019 continue
1020 elif state == 'git':
1021 gitpatches = values
1022 for gp in gitpatches:
1023 if gp.op in ('COPY', 'RENAME'):
1024 copyfile(gp.oldpath, gp.path)
1025 changed[gp.path] = (gp.op, gp)
1002 else:
1026 else:
1003 fname = current_file and current_file.fname or None
1027 raise util.Abort(_('unsupported parser state: %s') % state)
1004 raise PatchError(_("malformed patch %s %s") % (fname,
1005 current_hunk.desc))
1006 if current_file:
1007 current_file.close()
1008 if rejmerge:
1009 rejmerge(current_file)
1010 rejects += len(current_file.rej)
1011
1028
1012 if not rejects and hunknum == 0 and dopatch and not gitworkdone:
1029 rejects += closefile()
1013 raise NoHunks
1030
1014 if updatedir and git:
1031 if updatedir and gitpatches:
1015 updatedir(gitpatches)
1032 updatedir(gitpatches)
1016 if rejects:
1033 if rejects:
1017 return -1
1034 return -1
1018 return err
1035 return err
1019
1036
1020 def diffopts(ui, opts={}, untrusted=False):
1037 def diffopts(ui, opts={}, untrusted=False):
1021 def get(key, name=None):
1038 def get(key, name=None):
1022 return (opts.get(key) or
1039 return (opts.get(key) or
1023 ui.configbool('diff', name or key, None, untrusted=untrusted))
1040 ui.configbool('diff', name or key, None, untrusted=untrusted))
1024 return mdiff.diffopts(
1041 return mdiff.diffopts(
1025 text=opts.get('text'),
1042 text=opts.get('text'),
1026 git=get('git'),
1043 git=get('git'),
1027 nodates=get('nodates'),
1044 nodates=get('nodates'),
1028 showfunc=get('show_function', 'showfunc'),
1045 showfunc=get('show_function', 'showfunc'),
1029 ignorews=get('ignore_all_space', 'ignorews'),
1046 ignorews=get('ignore_all_space', 'ignorews'),
1030 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1047 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1031 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
1048 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
1032
1049
1033 def updatedir(ui, repo, patches):
1050 def updatedir(ui, repo, patches):
1034 '''Update dirstate after patch application according to metadata'''
1051 '''Update dirstate after patch application according to metadata'''
1035 if not patches:
1052 if not patches:
1036 return
1053 return
1037 copies = []
1054 copies = []
1038 removes = {}
1055 removes = {}
1039 cfiles = patches.keys()
1056 cfiles = patches.keys()
1040 cwd = repo.getcwd()
1057 cwd = repo.getcwd()
1041 if cwd:
1058 if cwd:
1042 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1059 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1043 for f in patches:
1060 for f in patches:
1044 ctype, gp = patches[f]
1061 ctype, gp = patches[f]
1045 if ctype == 'RENAME':
1062 if ctype == 'RENAME':
1046 copies.append((gp.oldpath, gp.path))
1063 copies.append((gp.oldpath, gp.path))
1047 removes[gp.oldpath] = 1
1064 removes[gp.oldpath] = 1
1048 elif ctype == 'COPY':
1065 elif ctype == 'COPY':
1049 copies.append((gp.oldpath, gp.path))
1066 copies.append((gp.oldpath, gp.path))
1050 elif ctype == 'DELETE':
1067 elif ctype == 'DELETE':
1051 removes[gp.path] = 1
1068 removes[gp.path] = 1
1052 for src, dst in copies:
1069 for src, dst in copies:
1053 repo.copy(src, dst)
1070 repo.copy(src, dst)
1054 removes = removes.keys()
1071 removes = removes.keys()
1055 if removes:
1072 if removes:
1056 removes.sort()
1073 removes.sort()
1057 repo.remove(removes, True)
1074 repo.remove(removes, True)
1058 for f in patches:
1075 for f in patches:
1059 ctype, gp = patches[f]
1076 ctype, gp = patches[f]
1060 if gp and gp.mode:
1077 if gp and gp.mode:
1061 x = gp.mode & 0100 != 0
1078 x = gp.mode & 0100 != 0
1062 l = gp.mode & 020000 != 0
1079 l = gp.mode & 020000 != 0
1063 dst = os.path.join(repo.root, gp.path)
1080 dst = os.path.join(repo.root, gp.path)
1064 # patch won't create empty files
1081 # patch won't create empty files
1065 if ctype == 'ADD' and not os.path.exists(dst):
1082 if ctype == 'ADD' and not os.path.exists(dst):
1066 repo.wwrite(gp.path, '', x and 'x' or '')
1083 repo.wwrite(gp.path, '', x and 'x' or '')
1067 else:
1084 else:
1068 util.set_link(dst, l)
1085 util.set_link(dst, l)
1069 if not l:
1086 if not l:
1070 util.set_exec(dst, x)
1087 util.set_exec(dst, x)
1071 cmdutil.addremove(repo, cfiles)
1088 cmdutil.addremove(repo, cfiles)
1072 files = patches.keys()
1089 files = patches.keys()
1073 files.extend([r for r in removes if r not in files])
1090 files.extend([r for r in removes if r not in files])
1074 files.sort()
1091 files.sort()
1075
1092
1076 return files
1093 return files
1077
1094
1078 def b85diff(to, tn):
1095 def b85diff(to, tn):
1079 '''print base85-encoded binary diff'''
1096 '''print base85-encoded binary diff'''
1080 def gitindex(text):
1097 def gitindex(text):
1081 if not text:
1098 if not text:
1082 return '0' * 40
1099 return '0' * 40
1083 l = len(text)
1100 l = len(text)
1084 s = sha.new('blob %d\0' % l)
1101 s = sha.new('blob %d\0' % l)
1085 s.update(text)
1102 s.update(text)
1086 return s.hexdigest()
1103 return s.hexdigest()
1087
1104
1088 def fmtline(line):
1105 def fmtline(line):
1089 l = len(line)
1106 l = len(line)
1090 if l <= 26:
1107 if l <= 26:
1091 l = chr(ord('A') + l - 1)
1108 l = chr(ord('A') + l - 1)
1092 else:
1109 else:
1093 l = chr(l - 26 + ord('a') - 1)
1110 l = chr(l - 26 + ord('a') - 1)
1094 return '%c%s\n' % (l, base85.b85encode(line, True))
1111 return '%c%s\n' % (l, base85.b85encode(line, True))
1095
1112
1096 def chunk(text, csize=52):
1113 def chunk(text, csize=52):
1097 l = len(text)
1114 l = len(text)
1098 i = 0
1115 i = 0
1099 while i < l:
1116 while i < l:
1100 yield text[i:i+csize]
1117 yield text[i:i+csize]
1101 i += csize
1118 i += csize
1102
1119
1103 tohash = gitindex(to)
1120 tohash = gitindex(to)
1104 tnhash = gitindex(tn)
1121 tnhash = gitindex(tn)
1105 if tohash == tnhash:
1122 if tohash == tnhash:
1106 return ""
1123 return ""
1107
1124
1108 # TODO: deltas
1125 # TODO: deltas
1109 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1126 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1110 (tohash, tnhash, len(tn))]
1127 (tohash, tnhash, len(tn))]
1111 for l in chunk(zlib.compress(tn)):
1128 for l in chunk(zlib.compress(tn)):
1112 ret.append(fmtline(l))
1129 ret.append(fmtline(l))
1113 ret.append('\n')
1130 ret.append('\n')
1114 return ''.join(ret)
1131 return ''.join(ret)
1115
1132
1116 def diff(repo, node1=None, node2=None, files=None, match=util.always,
1133 def diff(repo, node1=None, node2=None, files=None, match=util.always,
1117 fp=None, changes=None, opts=None):
1134 fp=None, changes=None, opts=None):
1118 '''print diff of changes to files between two nodes, or node and
1135 '''print diff of changes to files between two nodes, or node and
1119 working directory.
1136 working directory.
1120
1137
1121 if node1 is None, use first dirstate parent instead.
1138 if node1 is None, use first dirstate parent instead.
1122 if node2 is None, compare node1 with working directory.'''
1139 if node2 is None, compare node1 with working directory.'''
1123
1140
1124 if opts is None:
1141 if opts is None:
1125 opts = mdiff.defaultopts
1142 opts = mdiff.defaultopts
1126 if fp is None:
1143 if fp is None:
1127 fp = repo.ui
1144 fp = repo.ui
1128
1145
1129 if not node1:
1146 if not node1:
1130 node1 = repo.dirstate.parents()[0]
1147 node1 = repo.dirstate.parents()[0]
1131
1148
1132 ccache = {}
1149 ccache = {}
1133 def getctx(r):
1150 def getctx(r):
1134 if r not in ccache:
1151 if r not in ccache:
1135 ccache[r] = context.changectx(repo, r)
1152 ccache[r] = context.changectx(repo, r)
1136 return ccache[r]
1153 return ccache[r]
1137
1154
1138 flcache = {}
1155 flcache = {}
1139 def getfilectx(f, ctx):
1156 def getfilectx(f, ctx):
1140 flctx = ctx.filectx(f, filelog=flcache.get(f))
1157 flctx = ctx.filectx(f, filelog=flcache.get(f))
1141 if f not in flcache:
1158 if f not in flcache:
1142 flcache[f] = flctx._filelog
1159 flcache[f] = flctx._filelog
1143 return flctx
1160 return flctx
1144
1161
1145 # reading the data for node1 early allows it to play nicely
1162 # reading the data for node1 early allows it to play nicely
1146 # with repo.status and the revlog cache.
1163 # with repo.status and the revlog cache.
1147 ctx1 = context.changectx(repo, node1)
1164 ctx1 = context.changectx(repo, node1)
1148 # force manifest reading
1165 # force manifest reading
1149 man1 = ctx1.manifest()
1166 man1 = ctx1.manifest()
1150 date1 = util.datestr(ctx1.date())
1167 date1 = util.datestr(ctx1.date())
1151
1168
1152 if not changes:
1169 if not changes:
1153 changes = repo.status(node1, node2, files, match=match)[:5]
1170 changes = repo.status(node1, node2, files, match=match)[:5]
1154 modified, added, removed, deleted, unknown = changes
1171 modified, added, removed, deleted, unknown = changes
1155
1172
1156 if not modified and not added and not removed:
1173 if not modified and not added and not removed:
1157 return
1174 return
1158
1175
1159 if node2:
1176 if node2:
1160 ctx2 = context.changectx(repo, node2)
1177 ctx2 = context.changectx(repo, node2)
1161 execf2 = ctx2.manifest().execf
1178 execf2 = ctx2.manifest().execf
1162 linkf2 = ctx2.manifest().linkf
1179 linkf2 = ctx2.manifest().linkf
1163 else:
1180 else:
1164 ctx2 = context.workingctx(repo)
1181 ctx2 = context.workingctx(repo)
1165 execf2 = util.execfunc(repo.root, None)
1182 execf2 = util.execfunc(repo.root, None)
1166 linkf2 = util.linkfunc(repo.root, None)
1183 linkf2 = util.linkfunc(repo.root, None)
1167 if execf2 is None:
1184 if execf2 is None:
1168 mc = ctx2.parents()[0].manifest().copy()
1185 mc = ctx2.parents()[0].manifest().copy()
1169 execf2 = mc.execf
1186 execf2 = mc.execf
1170 linkf2 = mc.linkf
1187 linkf2 = mc.linkf
1171
1188
1172 # returns False if there was no rename between ctx1 and ctx2
1189 # returns False if there was no rename between ctx1 and ctx2
1173 # returns None if the file was created between ctx1 and ctx2
1190 # returns None if the file was created between ctx1 and ctx2
1174 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
1191 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
1175 # This will only really work if c1 is the Nth 1st parent of c2.
1192 # This will only really work if c1 is the Nth 1st parent of c2.
1176 def renamed(c1, c2, man, f):
1193 def renamed(c1, c2, man, f):
1177 startrev = c1.rev()
1194 startrev = c1.rev()
1178 c = c2
1195 c = c2
1179 crev = c.rev()
1196 crev = c.rev()
1180 if crev is None:
1197 if crev is None:
1181 crev = repo.changelog.count()
1198 crev = repo.changelog.count()
1182 orig = f
1199 orig = f
1183 files = (f,)
1200 files = (f,)
1184 while crev > startrev:
1201 while crev > startrev:
1185 if f in files:
1202 if f in files:
1186 try:
1203 try:
1187 src = getfilectx(f, c).renamed()
1204 src = getfilectx(f, c).renamed()
1188 except revlog.LookupError:
1205 except revlog.LookupError:
1189 return None
1206 return None
1190 if src:
1207 if src:
1191 f = src[0]
1208 f = src[0]
1192 crev = c.parents()[0].rev()
1209 crev = c.parents()[0].rev()
1193 # try to reuse
1210 # try to reuse
1194 c = getctx(crev)
1211 c = getctx(crev)
1195 files = c.files()
1212 files = c.files()
1196 if f not in man:
1213 if f not in man:
1197 return None
1214 return None
1198 if f == orig:
1215 if f == orig:
1199 return False
1216 return False
1200 return f
1217 return f
1201
1218
1202 if repo.ui.quiet:
1219 if repo.ui.quiet:
1203 r = None
1220 r = None
1204 else:
1221 else:
1205 hexfunc = repo.ui.debugflag and hex or short
1222 hexfunc = repo.ui.debugflag and hex or short
1206 r = [hexfunc(node) for node in [node1, node2] if node]
1223 r = [hexfunc(node) for node in [node1, node2] if node]
1207
1224
1208 if opts.git:
1225 if opts.git:
1209 copied = {}
1226 copied = {}
1210 c1, c2 = ctx1, ctx2
1227 c1, c2 = ctx1, ctx2
1211 files = added
1228 files = added
1212 man = man1
1229 man = man1
1213 if node2 and ctx1.rev() >= ctx2.rev():
1230 if node2 and ctx1.rev() >= ctx2.rev():
1214 # renamed() starts at c2 and walks back in history until c1.
1231 # renamed() starts at c2 and walks back in history until c1.
1215 # Since ctx1.rev() >= ctx2.rev(), invert ctx2 and ctx1 to
1232 # Since ctx1.rev() >= ctx2.rev(), invert ctx2 and ctx1 to
1216 # detect (inverted) copies.
1233 # detect (inverted) copies.
1217 c1, c2 = ctx2, ctx1
1234 c1, c2 = ctx2, ctx1
1218 files = removed
1235 files = removed
1219 man = ctx2.manifest()
1236 man = ctx2.manifest()
1220 for f in files:
1237 for f in files:
1221 src = renamed(c1, c2, man, f)
1238 src = renamed(c1, c2, man, f)
1222 if src:
1239 if src:
1223 copied[f] = src
1240 copied[f] = src
1224 if ctx1 == c2:
1241 if ctx1 == c2:
1225 # invert the copied dict
1242 # invert the copied dict
1226 copied = dict([(v, k) for (k, v) in copied.iteritems()])
1243 copied = dict([(v, k) for (k, v) in copied.iteritems()])
1227 # If we've renamed file foo to bar (copied['bar'] = 'foo'),
1244 # If we've renamed file foo to bar (copied['bar'] = 'foo'),
1228 # avoid showing a diff for foo if we're going to show
1245 # avoid showing a diff for foo if we're going to show
1229 # the rename to bar.
1246 # the rename to bar.
1230 srcs = [x[1] for x in copied.iteritems() if x[0] in added]
1247 srcs = [x[1] for x in copied.iteritems() if x[0] in added]
1231
1248
1232 all = modified + added + removed
1249 all = modified + added + removed
1233 all.sort()
1250 all.sort()
1234 gone = {}
1251 gone = {}
1235
1252
1236 for f in all:
1253 for f in all:
1237 to = None
1254 to = None
1238 tn = None
1255 tn = None
1239 dodiff = True
1256 dodiff = True
1240 header = []
1257 header = []
1241 if f in man1:
1258 if f in man1:
1242 to = getfilectx(f, ctx1).data()
1259 to = getfilectx(f, ctx1).data()
1243 if f not in removed:
1260 if f not in removed:
1244 tn = getfilectx(f, ctx2).data()
1261 tn = getfilectx(f, ctx2).data()
1245 a, b = f, f
1262 a, b = f, f
1246 if opts.git:
1263 if opts.git:
1247 def gitmode(x, l):
1264 def gitmode(x, l):
1248 return l and '120000' or (x and '100755' or '100644')
1265 return l and '120000' or (x and '100755' or '100644')
1249 def addmodehdr(header, omode, nmode):
1266 def addmodehdr(header, omode, nmode):
1250 if omode != nmode:
1267 if omode != nmode:
1251 header.append('old mode %s\n' % omode)
1268 header.append('old mode %s\n' % omode)
1252 header.append('new mode %s\n' % nmode)
1269 header.append('new mode %s\n' % nmode)
1253
1270
1254 if f in added:
1271 if f in added:
1255 mode = gitmode(execf2(f), linkf2(f))
1272 mode = gitmode(execf2(f), linkf2(f))
1256 if f in copied:
1273 if f in copied:
1257 a = copied[f]
1274 a = copied[f]
1258 omode = gitmode(man1.execf(a), man1.linkf(a))
1275 omode = gitmode(man1.execf(a), man1.linkf(a))
1259 addmodehdr(header, omode, mode)
1276 addmodehdr(header, omode, mode)
1260 if a in removed and a not in gone:
1277 if a in removed and a not in gone:
1261 op = 'rename'
1278 op = 'rename'
1262 gone[a] = 1
1279 gone[a] = 1
1263 else:
1280 else:
1264 op = 'copy'
1281 op = 'copy'
1265 header.append('%s from %s\n' % (op, a))
1282 header.append('%s from %s\n' % (op, a))
1266 header.append('%s to %s\n' % (op, f))
1283 header.append('%s to %s\n' % (op, f))
1267 to = getfilectx(a, ctx1).data()
1284 to = getfilectx(a, ctx1).data()
1268 else:
1285 else:
1269 header.append('new file mode %s\n' % mode)
1286 header.append('new file mode %s\n' % mode)
1270 if util.binary(tn):
1287 if util.binary(tn):
1271 dodiff = 'binary'
1288 dodiff = 'binary'
1272 elif f in removed:
1289 elif f in removed:
1273 if f in srcs:
1290 if f in srcs:
1274 dodiff = False
1291 dodiff = False
1275 else:
1292 else:
1276 mode = gitmode(man1.execf(f), man1.linkf(f))
1293 mode = gitmode(man1.execf(f), man1.linkf(f))
1277 header.append('deleted file mode %s\n' % mode)
1294 header.append('deleted file mode %s\n' % mode)
1278 else:
1295 else:
1279 omode = gitmode(man1.execf(f), man1.linkf(f))
1296 omode = gitmode(man1.execf(f), man1.linkf(f))
1280 nmode = gitmode(execf2(f), linkf2(f))
1297 nmode = gitmode(execf2(f), linkf2(f))
1281 addmodehdr(header, omode, nmode)
1298 addmodehdr(header, omode, nmode)
1282 if util.binary(to) or util.binary(tn):
1299 if util.binary(to) or util.binary(tn):
1283 dodiff = 'binary'
1300 dodiff = 'binary'
1284 r = None
1301 r = None
1285 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
1302 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
1286 if dodiff:
1303 if dodiff:
1287 if dodiff == 'binary':
1304 if dodiff == 'binary':
1288 text = b85diff(to, tn)
1305 text = b85diff(to, tn)
1289 else:
1306 else:
1290 text = mdiff.unidiff(to, date1,
1307 text = mdiff.unidiff(to, date1,
1291 # ctx2 date may be dynamic
1308 # ctx2 date may be dynamic
1292 tn, util.datestr(ctx2.date()),
1309 tn, util.datestr(ctx2.date()),
1293 a, b, r, opts=opts)
1310 a, b, r, opts=opts)
1294 if text or len(header) > 1:
1311 if text or len(header) > 1:
1295 fp.write(''.join(header))
1312 fp.write(''.join(header))
1296 fp.write(text)
1313 fp.write(text)
1297
1314
1298 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1315 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1299 opts=None):
1316 opts=None):
1300 '''export changesets as hg patches.'''
1317 '''export changesets as hg patches.'''
1301
1318
1302 total = len(revs)
1319 total = len(revs)
1303 revwidth = max([len(str(rev)) for rev in revs])
1320 revwidth = max([len(str(rev)) for rev in revs])
1304
1321
1305 def single(rev, seqno, fp):
1322 def single(rev, seqno, fp):
1306 ctx = repo.changectx(rev)
1323 ctx = repo.changectx(rev)
1307 node = ctx.node()
1324 node = ctx.node()
1308 parents = [p.node() for p in ctx.parents() if p]
1325 parents = [p.node() for p in ctx.parents() if p]
1309 branch = ctx.branch()
1326 branch = ctx.branch()
1310 if switch_parent:
1327 if switch_parent:
1311 parents.reverse()
1328 parents.reverse()
1312 prev = (parents and parents[0]) or nullid
1329 prev = (parents and parents[0]) or nullid
1313
1330
1314 if not fp:
1331 if not fp:
1315 fp = cmdutil.make_file(repo, template, node, total=total,
1332 fp = cmdutil.make_file(repo, template, node, total=total,
1316 seqno=seqno, revwidth=revwidth)
1333 seqno=seqno, revwidth=revwidth)
1317 if fp != sys.stdout and hasattr(fp, 'name'):
1334 if fp != sys.stdout and hasattr(fp, 'name'):
1318 repo.ui.note("%s\n" % fp.name)
1335 repo.ui.note("%s\n" % fp.name)
1319
1336
1320 fp.write("# HG changeset patch\n")
1337 fp.write("# HG changeset patch\n")
1321 fp.write("# User %s\n" % ctx.user())
1338 fp.write("# User %s\n" % ctx.user())
1322 fp.write("# Date %d %d\n" % ctx.date())
1339 fp.write("# Date %d %d\n" % ctx.date())
1323 if branch and (branch != 'default'):
1340 if branch and (branch != 'default'):
1324 fp.write("# Branch %s\n" % branch)
1341 fp.write("# Branch %s\n" % branch)
1325 fp.write("# Node ID %s\n" % hex(node))
1342 fp.write("# Node ID %s\n" % hex(node))
1326 fp.write("# Parent %s\n" % hex(prev))
1343 fp.write("# Parent %s\n" % hex(prev))
1327 if len(parents) > 1:
1344 if len(parents) > 1:
1328 fp.write("# Parent %s\n" % hex(parents[1]))
1345 fp.write("# Parent %s\n" % hex(parents[1]))
1329 fp.write(ctx.description().rstrip())
1346 fp.write(ctx.description().rstrip())
1330 fp.write("\n\n")
1347 fp.write("\n\n")
1331
1348
1332 diff(repo, prev, node, fp=fp, opts=opts)
1349 diff(repo, prev, node, fp=fp, opts=opts)
1333 if fp not in (sys.stdout, repo.ui):
1350 if fp not in (sys.stdout, repo.ui):
1334 fp.close()
1351 fp.close()
1335
1352
1336 for seqno, rev in enumerate(revs):
1353 for seqno, rev in enumerate(revs):
1337 single(rev, seqno+1, fp)
1354 single(rev, seqno+1, fp)
1338
1355
1339 def diffstat(patchlines):
1356 def diffstat(patchlines):
1340 if not util.find_exe('diffstat'):
1357 if not util.find_exe('diffstat'):
1341 return
1358 return
1342 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
1359 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
1343 try:
1360 try:
1344 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
1361 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
1345 try:
1362 try:
1346 for line in patchlines: print >> p.tochild, line
1363 for line in patchlines: print >> p.tochild, line
1347 p.tochild.close()
1364 p.tochild.close()
1348 if p.wait(): return
1365 if p.wait(): return
1349 fp = os.fdopen(fd, 'r')
1366 fp = os.fdopen(fd, 'r')
1350 stat = []
1367 stat = []
1351 for line in fp: stat.append(line.lstrip())
1368 for line in fp: stat.append(line.lstrip())
1352 last = stat.pop()
1369 last = stat.pop()
1353 stat.insert(0, last)
1370 stat.insert(0, last)
1354 stat = ''.join(stat)
1371 stat = ''.join(stat)
1355 if stat.startswith('0 files'): raise ValueError
1372 if stat.startswith('0 files'): raise ValueError
1356 return stat
1373 return stat
1357 except: raise
1374 except: raise
1358 finally:
1375 finally:
1359 try: os.unlink(name)
1376 try: os.unlink(name)
1360 except: pass
1377 except: pass
@@ -1,25 +1,24 b''
1 adding b
1 adding b
2 Patch queue now empty
2 Patch queue now empty
3 % push patch with missing target
3 % push patch with missing target
4 applying changeb
4 applying changeb
5 unable to find b or b for patching
5 unable to find b or b for patching
6 unable to find b or b for patching
7 patch failed, unable to continue (try -v)
6 patch failed, unable to continue (try -v)
8 patch failed, rejects left in working dir
7 patch failed, rejects left in working dir
9 Errors during apply, please fix and refresh changeb
8 Errors during apply, please fix and refresh changeb
10 % display added files
9 % display added files
11 a
10 a
12 c
11 c
13 adding b
12 adding b
14 Patch queue now empty
13 Patch queue now empty
15 % push git patch with missing target
14 % push git patch with missing target
16 applying changeb
15 applying changeb
17 unable to find b or b for patching
16 unable to find b or b for patching
18 patch failed, unable to continue (try -v)
17 patch failed, unable to continue (try -v)
19 b: No such file or directory
18 b: No such file or directory
20 b not tracked!
19 b not tracked!
21 patch failed, rejects left in working dir
20 patch failed, rejects left in working dir
22 Errors during apply, please fix and refresh changeb
21 Errors during apply, please fix and refresh changeb
23 % display added files
22 % display added files
24 a
23 a
25 c
24 c
General Comments 0
You need to be logged in to leave comments. Login now