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