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