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