##// END OF EJS Templates
patch: stop updating changed files set in applydiff()...
Patrick Mezard -
r14565:3cacc232 default
parent child Browse files
Show More
@@ -1,1785 +1,1781 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 of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import cStringIO, email.Parser, os, errno, re
9 import cStringIO, email.Parser, os, errno, re
10 import tempfile, zlib, shutil
10 import tempfile, zlib, shutil
11
11
12 from i18n import _
12 from i18n import _
13 from node import hex, nullid, short
13 from node import hex, nullid, short
14 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding
14 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding
15
15
16 gitre = re.compile('diff --git a/(.*) b/(.*)')
16 gitre = re.compile('diff --git a/(.*) b/(.*)')
17
17
18 class PatchError(Exception):
18 class PatchError(Exception):
19 pass
19 pass
20
20
21
21
22 # public functions
22 # public functions
23
23
24 def split(stream):
24 def split(stream):
25 '''return an iterator of individual patches from a stream'''
25 '''return an iterator of individual patches from a stream'''
26 def isheader(line, inheader):
26 def isheader(line, inheader):
27 if inheader and line[0] in (' ', '\t'):
27 if inheader and line[0] in (' ', '\t'):
28 # continuation
28 # continuation
29 return True
29 return True
30 if line[0] in (' ', '-', '+'):
30 if line[0] in (' ', '-', '+'):
31 # diff line - don't check for header pattern in there
31 # diff line - don't check for header pattern in there
32 return False
32 return False
33 l = line.split(': ', 1)
33 l = line.split(': ', 1)
34 return len(l) == 2 and ' ' not in l[0]
34 return len(l) == 2 and ' ' not in l[0]
35
35
36 def chunk(lines):
36 def chunk(lines):
37 return cStringIO.StringIO(''.join(lines))
37 return cStringIO.StringIO(''.join(lines))
38
38
39 def hgsplit(stream, cur):
39 def hgsplit(stream, cur):
40 inheader = True
40 inheader = True
41
41
42 for line in stream:
42 for line in stream:
43 if not line.strip():
43 if not line.strip():
44 inheader = False
44 inheader = False
45 if not inheader and line.startswith('# HG changeset patch'):
45 if not inheader and line.startswith('# HG changeset patch'):
46 yield chunk(cur)
46 yield chunk(cur)
47 cur = []
47 cur = []
48 inheader = True
48 inheader = True
49
49
50 cur.append(line)
50 cur.append(line)
51
51
52 if cur:
52 if cur:
53 yield chunk(cur)
53 yield chunk(cur)
54
54
55 def mboxsplit(stream, cur):
55 def mboxsplit(stream, cur):
56 for line in stream:
56 for line in stream:
57 if line.startswith('From '):
57 if line.startswith('From '):
58 for c in split(chunk(cur[1:])):
58 for c in split(chunk(cur[1:])):
59 yield c
59 yield c
60 cur = []
60 cur = []
61
61
62 cur.append(line)
62 cur.append(line)
63
63
64 if cur:
64 if cur:
65 for c in split(chunk(cur[1:])):
65 for c in split(chunk(cur[1:])):
66 yield c
66 yield c
67
67
68 def mimesplit(stream, cur):
68 def mimesplit(stream, cur):
69 def msgfp(m):
69 def msgfp(m):
70 fp = cStringIO.StringIO()
70 fp = cStringIO.StringIO()
71 g = email.Generator.Generator(fp, mangle_from_=False)
71 g = email.Generator.Generator(fp, mangle_from_=False)
72 g.flatten(m)
72 g.flatten(m)
73 fp.seek(0)
73 fp.seek(0)
74 return fp
74 return fp
75
75
76 for line in stream:
76 for line in stream:
77 cur.append(line)
77 cur.append(line)
78 c = chunk(cur)
78 c = chunk(cur)
79
79
80 m = email.Parser.Parser().parse(c)
80 m = email.Parser.Parser().parse(c)
81 if not m.is_multipart():
81 if not m.is_multipart():
82 yield msgfp(m)
82 yield msgfp(m)
83 else:
83 else:
84 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
84 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
85 for part in m.walk():
85 for part in m.walk():
86 ct = part.get_content_type()
86 ct = part.get_content_type()
87 if ct not in ok_types:
87 if ct not in ok_types:
88 continue
88 continue
89 yield msgfp(part)
89 yield msgfp(part)
90
90
91 def headersplit(stream, cur):
91 def headersplit(stream, cur):
92 inheader = False
92 inheader = False
93
93
94 for line in stream:
94 for line in stream:
95 if not inheader and isheader(line, inheader):
95 if not inheader and isheader(line, inheader):
96 yield chunk(cur)
96 yield chunk(cur)
97 cur = []
97 cur = []
98 inheader = True
98 inheader = True
99 if inheader and not isheader(line, inheader):
99 if inheader and not isheader(line, inheader):
100 inheader = False
100 inheader = False
101
101
102 cur.append(line)
102 cur.append(line)
103
103
104 if cur:
104 if cur:
105 yield chunk(cur)
105 yield chunk(cur)
106
106
107 def remainder(cur):
107 def remainder(cur):
108 yield chunk(cur)
108 yield chunk(cur)
109
109
110 class fiter(object):
110 class fiter(object):
111 def __init__(self, fp):
111 def __init__(self, fp):
112 self.fp = fp
112 self.fp = fp
113
113
114 def __iter__(self):
114 def __iter__(self):
115 return self
115 return self
116
116
117 def next(self):
117 def next(self):
118 l = self.fp.readline()
118 l = self.fp.readline()
119 if not l:
119 if not l:
120 raise StopIteration
120 raise StopIteration
121 return l
121 return l
122
122
123 inheader = False
123 inheader = False
124 cur = []
124 cur = []
125
125
126 mimeheaders = ['content-type']
126 mimeheaders = ['content-type']
127
127
128 if not hasattr(stream, 'next'):
128 if not hasattr(stream, 'next'):
129 # http responses, for example, have readline but not next
129 # http responses, for example, have readline but not next
130 stream = fiter(stream)
130 stream = fiter(stream)
131
131
132 for line in stream:
132 for line in stream:
133 cur.append(line)
133 cur.append(line)
134 if line.startswith('# HG changeset patch'):
134 if line.startswith('# HG changeset patch'):
135 return hgsplit(stream, cur)
135 return hgsplit(stream, cur)
136 elif line.startswith('From '):
136 elif line.startswith('From '):
137 return mboxsplit(stream, cur)
137 return mboxsplit(stream, cur)
138 elif isheader(line, inheader):
138 elif isheader(line, inheader):
139 inheader = True
139 inheader = True
140 if line.split(':', 1)[0].lower() in mimeheaders:
140 if line.split(':', 1)[0].lower() in mimeheaders:
141 # let email parser handle this
141 # let email parser handle this
142 return mimesplit(stream, cur)
142 return mimesplit(stream, cur)
143 elif line.startswith('--- ') and inheader:
143 elif line.startswith('--- ') and inheader:
144 # No evil headers seen by diff start, split by hand
144 # No evil headers seen by diff start, split by hand
145 return headersplit(stream, cur)
145 return headersplit(stream, cur)
146 # Not enough info, keep reading
146 # Not enough info, keep reading
147
147
148 # if we are here, we have a very plain patch
148 # if we are here, we have a very plain patch
149 return remainder(cur)
149 return remainder(cur)
150
150
151 def extract(ui, fileobj):
151 def extract(ui, fileobj):
152 '''extract patch from data read from fileobj.
152 '''extract patch from data read from fileobj.
153
153
154 patch can be a normal patch or contained in an email message.
154 patch can be a normal patch or contained in an email message.
155
155
156 return tuple (filename, message, user, date, branch, node, p1, p2).
156 return tuple (filename, message, user, date, branch, node, p1, p2).
157 Any item in the returned tuple can be None. If filename is None,
157 Any item in the returned tuple can be None. If filename is None,
158 fileobj did not contain a patch. Caller must unlink filename when done.'''
158 fileobj did not contain a patch. Caller must unlink filename when done.'''
159
159
160 # attempt to detect the start of a patch
160 # attempt to detect the start of a patch
161 # (this heuristic is borrowed from quilt)
161 # (this heuristic is borrowed from quilt)
162 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
162 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
163 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
163 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
164 r'---[ \t].*?^\+\+\+[ \t]|'
164 r'---[ \t].*?^\+\+\+[ \t]|'
165 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
165 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
166
166
167 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
167 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
168 tmpfp = os.fdopen(fd, 'w')
168 tmpfp = os.fdopen(fd, 'w')
169 try:
169 try:
170 msg = email.Parser.Parser().parse(fileobj)
170 msg = email.Parser.Parser().parse(fileobj)
171
171
172 subject = msg['Subject']
172 subject = msg['Subject']
173 user = msg['From']
173 user = msg['From']
174 if not subject and not user:
174 if not subject and not user:
175 # Not an email, restore parsed headers if any
175 # Not an email, restore parsed headers if any
176 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
176 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
177
177
178 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
178 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
179 # should try to parse msg['Date']
179 # should try to parse msg['Date']
180 date = None
180 date = None
181 nodeid = None
181 nodeid = None
182 branch = None
182 branch = None
183 parents = []
183 parents = []
184
184
185 if subject:
185 if subject:
186 if subject.startswith('[PATCH'):
186 if subject.startswith('[PATCH'):
187 pend = subject.find(']')
187 pend = subject.find(']')
188 if pend >= 0:
188 if pend >= 0:
189 subject = subject[pend + 1:].lstrip()
189 subject = subject[pend + 1:].lstrip()
190 subject = subject.replace('\n\t', ' ')
190 subject = subject.replace('\n\t', ' ')
191 ui.debug('Subject: %s\n' % subject)
191 ui.debug('Subject: %s\n' % subject)
192 if user:
192 if user:
193 ui.debug('From: %s\n' % user)
193 ui.debug('From: %s\n' % user)
194 diffs_seen = 0
194 diffs_seen = 0
195 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
195 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
196 message = ''
196 message = ''
197 for part in msg.walk():
197 for part in msg.walk():
198 content_type = part.get_content_type()
198 content_type = part.get_content_type()
199 ui.debug('Content-Type: %s\n' % content_type)
199 ui.debug('Content-Type: %s\n' % content_type)
200 if content_type not in ok_types:
200 if content_type not in ok_types:
201 continue
201 continue
202 payload = part.get_payload(decode=True)
202 payload = part.get_payload(decode=True)
203 m = diffre.search(payload)
203 m = diffre.search(payload)
204 if m:
204 if m:
205 hgpatch = False
205 hgpatch = False
206 hgpatchheader = False
206 hgpatchheader = False
207 ignoretext = False
207 ignoretext = False
208
208
209 ui.debug('found patch at byte %d\n' % m.start(0))
209 ui.debug('found patch at byte %d\n' % m.start(0))
210 diffs_seen += 1
210 diffs_seen += 1
211 cfp = cStringIO.StringIO()
211 cfp = cStringIO.StringIO()
212 for line in payload[:m.start(0)].splitlines():
212 for line in payload[:m.start(0)].splitlines():
213 if line.startswith('# HG changeset patch') and not hgpatch:
213 if line.startswith('# HG changeset patch') and not hgpatch:
214 ui.debug('patch generated by hg export\n')
214 ui.debug('patch generated by hg export\n')
215 hgpatch = True
215 hgpatch = True
216 hgpatchheader = True
216 hgpatchheader = True
217 # drop earlier commit message content
217 # drop earlier commit message content
218 cfp.seek(0)
218 cfp.seek(0)
219 cfp.truncate()
219 cfp.truncate()
220 subject = None
220 subject = None
221 elif hgpatchheader:
221 elif hgpatchheader:
222 if line.startswith('# User '):
222 if line.startswith('# User '):
223 user = line[7:]
223 user = line[7:]
224 ui.debug('From: %s\n' % user)
224 ui.debug('From: %s\n' % user)
225 elif line.startswith("# Date "):
225 elif line.startswith("# Date "):
226 date = line[7:]
226 date = line[7:]
227 elif line.startswith("# Branch "):
227 elif line.startswith("# Branch "):
228 branch = line[9:]
228 branch = line[9:]
229 elif line.startswith("# Node ID "):
229 elif line.startswith("# Node ID "):
230 nodeid = line[10:]
230 nodeid = line[10:]
231 elif line.startswith("# Parent "):
231 elif line.startswith("# Parent "):
232 parents.append(line[10:])
232 parents.append(line[10:])
233 elif not line.startswith("# "):
233 elif not line.startswith("# "):
234 hgpatchheader = False
234 hgpatchheader = False
235 elif line == '---' and gitsendmail:
235 elif line == '---' and gitsendmail:
236 ignoretext = True
236 ignoretext = True
237 if not hgpatchheader and not ignoretext:
237 if not hgpatchheader and not ignoretext:
238 cfp.write(line)
238 cfp.write(line)
239 cfp.write('\n')
239 cfp.write('\n')
240 message = cfp.getvalue()
240 message = cfp.getvalue()
241 if tmpfp:
241 if tmpfp:
242 tmpfp.write(payload)
242 tmpfp.write(payload)
243 if not payload.endswith('\n'):
243 if not payload.endswith('\n'):
244 tmpfp.write('\n')
244 tmpfp.write('\n')
245 elif not diffs_seen and message and content_type == 'text/plain':
245 elif not diffs_seen and message and content_type == 'text/plain':
246 message += '\n' + payload
246 message += '\n' + payload
247 except:
247 except:
248 tmpfp.close()
248 tmpfp.close()
249 os.unlink(tmpname)
249 os.unlink(tmpname)
250 raise
250 raise
251
251
252 if subject and not message.startswith(subject):
252 if subject and not message.startswith(subject):
253 message = '%s\n%s' % (subject, message)
253 message = '%s\n%s' % (subject, message)
254 tmpfp.close()
254 tmpfp.close()
255 if not diffs_seen:
255 if not diffs_seen:
256 os.unlink(tmpname)
256 os.unlink(tmpname)
257 return None, message, user, date, branch, None, None, None
257 return None, message, user, date, branch, None, None, None
258 p1 = parents and parents.pop(0) or None
258 p1 = parents and parents.pop(0) or None
259 p2 = parents and parents.pop(0) or None
259 p2 = parents and parents.pop(0) or None
260 return tmpname, message, user, date, branch, nodeid, p1, p2
260 return tmpname, message, user, date, branch, nodeid, p1, p2
261
261
262 class patchmeta(object):
262 class patchmeta(object):
263 """Patched file metadata
263 """Patched file metadata
264
264
265 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
265 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
266 or COPY. 'path' is patched file path. 'oldpath' is set to the
266 or COPY. 'path' is patched file path. 'oldpath' is set to the
267 origin file when 'op' is either COPY or RENAME, None otherwise. If
267 origin file when 'op' is either COPY or RENAME, None otherwise. If
268 file mode is changed, 'mode' is a tuple (islink, isexec) where
268 file mode is changed, 'mode' is a tuple (islink, isexec) where
269 'islink' is True if the file is a symlink and 'isexec' is True if
269 'islink' is True if the file is a symlink and 'isexec' is True if
270 the file is executable. Otherwise, 'mode' is None.
270 the file is executable. Otherwise, 'mode' is None.
271 """
271 """
272 def __init__(self, path):
272 def __init__(self, path):
273 self.path = path
273 self.path = path
274 self.oldpath = None
274 self.oldpath = None
275 self.mode = None
275 self.mode = None
276 self.op = 'MODIFY'
276 self.op = 'MODIFY'
277 self.binary = False
277 self.binary = False
278
278
279 def setmode(self, mode):
279 def setmode(self, mode):
280 islink = mode & 020000
280 islink = mode & 020000
281 isexec = mode & 0100
281 isexec = mode & 0100
282 self.mode = (islink, isexec)
282 self.mode = (islink, isexec)
283
283
284 def __repr__(self):
284 def __repr__(self):
285 return "<patchmeta %s %r>" % (self.op, self.path)
285 return "<patchmeta %s %r>" % (self.op, self.path)
286
286
287 def readgitpatch(lr):
287 def readgitpatch(lr):
288 """extract git-style metadata about patches from <patchname>"""
288 """extract git-style metadata about patches from <patchname>"""
289
289
290 # Filter patch for git information
290 # Filter patch for git information
291 gp = None
291 gp = None
292 gitpatches = []
292 gitpatches = []
293 for line in lr:
293 for line in lr:
294 line = line.rstrip(' \r\n')
294 line = line.rstrip(' \r\n')
295 if line.startswith('diff --git'):
295 if line.startswith('diff --git'):
296 m = gitre.match(line)
296 m = gitre.match(line)
297 if m:
297 if m:
298 if gp:
298 if gp:
299 gitpatches.append(gp)
299 gitpatches.append(gp)
300 dst = m.group(2)
300 dst = m.group(2)
301 gp = patchmeta(dst)
301 gp = patchmeta(dst)
302 elif gp:
302 elif gp:
303 if line.startswith('--- '):
303 if line.startswith('--- '):
304 gitpatches.append(gp)
304 gitpatches.append(gp)
305 gp = None
305 gp = None
306 continue
306 continue
307 if line.startswith('rename from '):
307 if line.startswith('rename from '):
308 gp.op = 'RENAME'
308 gp.op = 'RENAME'
309 gp.oldpath = line[12:]
309 gp.oldpath = line[12:]
310 elif line.startswith('rename to '):
310 elif line.startswith('rename to '):
311 gp.path = line[10:]
311 gp.path = line[10:]
312 elif line.startswith('copy from '):
312 elif line.startswith('copy from '):
313 gp.op = 'COPY'
313 gp.op = 'COPY'
314 gp.oldpath = line[10:]
314 gp.oldpath = line[10:]
315 elif line.startswith('copy to '):
315 elif line.startswith('copy to '):
316 gp.path = line[8:]
316 gp.path = line[8:]
317 elif line.startswith('deleted file'):
317 elif line.startswith('deleted file'):
318 gp.op = 'DELETE'
318 gp.op = 'DELETE'
319 elif line.startswith('new file mode '):
319 elif line.startswith('new file mode '):
320 gp.op = 'ADD'
320 gp.op = 'ADD'
321 gp.setmode(int(line[-6:], 8))
321 gp.setmode(int(line[-6:], 8))
322 elif line.startswith('new mode '):
322 elif line.startswith('new mode '):
323 gp.setmode(int(line[-6:], 8))
323 gp.setmode(int(line[-6:], 8))
324 elif line.startswith('GIT binary patch'):
324 elif line.startswith('GIT binary patch'):
325 gp.binary = True
325 gp.binary = True
326 if gp:
326 if gp:
327 gitpatches.append(gp)
327 gitpatches.append(gp)
328
328
329 return gitpatches
329 return gitpatches
330
330
331 class linereader(object):
331 class linereader(object):
332 # simple class to allow pushing lines back into the input stream
332 # simple class to allow pushing lines back into the input stream
333 def __init__(self, fp):
333 def __init__(self, fp):
334 self.fp = fp
334 self.fp = fp
335 self.buf = []
335 self.buf = []
336
336
337 def push(self, line):
337 def push(self, line):
338 if line is not None:
338 if line is not None:
339 self.buf.append(line)
339 self.buf.append(line)
340
340
341 def readline(self):
341 def readline(self):
342 if self.buf:
342 if self.buf:
343 l = self.buf[0]
343 l = self.buf[0]
344 del self.buf[0]
344 del self.buf[0]
345 return l
345 return l
346 return self.fp.readline()
346 return self.fp.readline()
347
347
348 def __iter__(self):
348 def __iter__(self):
349 while True:
349 while True:
350 l = self.readline()
350 l = self.readline()
351 if not l:
351 if not l:
352 break
352 break
353 yield l
353 yield l
354
354
355 class abstractbackend(object):
355 class abstractbackend(object):
356 def __init__(self, ui):
356 def __init__(self, ui):
357 self.ui = ui
357 self.ui = ui
358
358
359 def getfile(self, fname):
359 def getfile(self, fname):
360 """Return target file data and flags as a (data, (islink,
360 """Return target file data and flags as a (data, (islink,
361 isexec)) tuple.
361 isexec)) tuple.
362 """
362 """
363 raise NotImplementedError
363 raise NotImplementedError
364
364
365 def setfile(self, fname, data, mode, copysource):
365 def setfile(self, fname, data, mode, copysource):
366 """Write data to target file fname and set its mode. mode is a
366 """Write data to target file fname and set its mode. mode is a
367 (islink, isexec) tuple. If data is None, the file content should
367 (islink, isexec) tuple. If data is None, the file content should
368 be left unchanged. If the file is modified after being copied,
368 be left unchanged. If the file is modified after being copied,
369 copysource is set to the original file name.
369 copysource is set to the original file name.
370 """
370 """
371 raise NotImplementedError
371 raise NotImplementedError
372
372
373 def unlink(self, fname):
373 def unlink(self, fname):
374 """Unlink target file."""
374 """Unlink target file."""
375 raise NotImplementedError
375 raise NotImplementedError
376
376
377 def writerej(self, fname, failed, total, lines):
377 def writerej(self, fname, failed, total, lines):
378 """Write rejected lines for fname. total is the number of hunks
378 """Write rejected lines for fname. total is the number of hunks
379 which failed to apply and total the total number of hunks for this
379 which failed to apply and total the total number of hunks for this
380 files.
380 files.
381 """
381 """
382 pass
382 pass
383
383
384 def exists(self, fname):
384 def exists(self, fname):
385 raise NotImplementedError
385 raise NotImplementedError
386
386
387 class fsbackend(abstractbackend):
387 class fsbackend(abstractbackend):
388 def __init__(self, ui, basedir):
388 def __init__(self, ui, basedir):
389 super(fsbackend, self).__init__(ui)
389 super(fsbackend, self).__init__(ui)
390 self.opener = scmutil.opener(basedir)
390 self.opener = scmutil.opener(basedir)
391
391
392 def _join(self, f):
392 def _join(self, f):
393 return os.path.join(self.opener.base, f)
393 return os.path.join(self.opener.base, f)
394
394
395 def getfile(self, fname):
395 def getfile(self, fname):
396 path = self._join(fname)
396 path = self._join(fname)
397 if os.path.islink(path):
397 if os.path.islink(path):
398 return (os.readlink(path), (True, False))
398 return (os.readlink(path), (True, False))
399 isexec = False
399 isexec = False
400 try:
400 try:
401 isexec = os.lstat(path).st_mode & 0100 != 0
401 isexec = os.lstat(path).st_mode & 0100 != 0
402 except OSError, e:
402 except OSError, e:
403 if e.errno != errno.ENOENT:
403 if e.errno != errno.ENOENT:
404 raise
404 raise
405 return (self.opener.read(fname), (False, isexec))
405 return (self.opener.read(fname), (False, isexec))
406
406
407 def setfile(self, fname, data, mode, copysource):
407 def setfile(self, fname, data, mode, copysource):
408 islink, isexec = mode
408 islink, isexec = mode
409 if data is None:
409 if data is None:
410 util.setflags(self._join(fname), islink, isexec)
410 util.setflags(self._join(fname), islink, isexec)
411 return
411 return
412 if islink:
412 if islink:
413 self.opener.symlink(data, fname)
413 self.opener.symlink(data, fname)
414 else:
414 else:
415 self.opener.write(fname, data)
415 self.opener.write(fname, data)
416 if isexec:
416 if isexec:
417 util.setflags(self._join(fname), False, True)
417 util.setflags(self._join(fname), False, True)
418
418
419 def unlink(self, fname):
419 def unlink(self, fname):
420 try:
420 try:
421 util.unlinkpath(self._join(fname))
421 util.unlinkpath(self._join(fname))
422 except OSError, inst:
422 except OSError, inst:
423 if inst.errno != errno.ENOENT:
423 if inst.errno != errno.ENOENT:
424 raise
424 raise
425
425
426 def writerej(self, fname, failed, total, lines):
426 def writerej(self, fname, failed, total, lines):
427 fname = fname + ".rej"
427 fname = fname + ".rej"
428 self.ui.warn(
428 self.ui.warn(
429 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
429 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
430 (failed, total, fname))
430 (failed, total, fname))
431 fp = self.opener(fname, 'w')
431 fp = self.opener(fname, 'w')
432 fp.writelines(lines)
432 fp.writelines(lines)
433 fp.close()
433 fp.close()
434
434
435 def exists(self, fname):
435 def exists(self, fname):
436 return os.path.lexists(self._join(fname))
436 return os.path.lexists(self._join(fname))
437
437
438 class workingbackend(fsbackend):
438 class workingbackend(fsbackend):
439 def __init__(self, ui, repo, similarity):
439 def __init__(self, ui, repo, similarity):
440 super(workingbackend, self).__init__(ui, repo.root)
440 super(workingbackend, self).__init__(ui, repo.root)
441 self.repo = repo
441 self.repo = repo
442 self.similarity = similarity
442 self.similarity = similarity
443 self.removed = set()
443 self.removed = set()
444 self.changed = set()
444 self.changed = set()
445 self.copied = []
445 self.copied = []
446
446
447 def _checkknown(self, fname):
447 def _checkknown(self, fname):
448 if self.repo.dirstate[fname] == '?' and self.exists(fname):
448 if self.repo.dirstate[fname] == '?' and self.exists(fname):
449 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
449 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
450
450
451 def setfile(self, fname, data, mode, copysource):
451 def setfile(self, fname, data, mode, copysource):
452 self._checkknown(fname)
452 self._checkknown(fname)
453 super(workingbackend, self).setfile(fname, data, mode, copysource)
453 super(workingbackend, self).setfile(fname, data, mode, copysource)
454 if copysource is not None:
454 if copysource is not None:
455 self.copied.append((copysource, fname))
455 self.copied.append((copysource, fname))
456 self.changed.add(fname)
456 self.changed.add(fname)
457
457
458 def unlink(self, fname):
458 def unlink(self, fname):
459 self._checkknown(fname)
459 self._checkknown(fname)
460 super(workingbackend, self).unlink(fname)
460 super(workingbackend, self).unlink(fname)
461 self.removed.add(fname)
461 self.removed.add(fname)
462 self.changed.add(fname)
462 self.changed.add(fname)
463
463
464 def close(self):
464 def close(self):
465 wctx = self.repo[None]
465 wctx = self.repo[None]
466 addremoved = set(self.changed)
466 addremoved = set(self.changed)
467 for src, dst in self.copied:
467 for src, dst in self.copied:
468 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
468 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
469 addremoved.discard(src)
469 addremoved.discard(src)
470 if (not self.similarity) and self.removed:
470 if (not self.similarity) and self.removed:
471 wctx.forget(sorted(self.removed))
471 wctx.forget(sorted(self.removed))
472 if addremoved:
472 if addremoved:
473 cwd = self.repo.getcwd()
473 cwd = self.repo.getcwd()
474 if cwd:
474 if cwd:
475 addremoved = [util.pathto(self.repo.root, cwd, f)
475 addremoved = [util.pathto(self.repo.root, cwd, f)
476 for f in addremoved]
476 for f in addremoved]
477 scmutil.addremove(self.repo, addremoved, similarity=self.similarity)
477 scmutil.addremove(self.repo, addremoved, similarity=self.similarity)
478 return sorted(self.changed)
478 return sorted(self.changed)
479
479
480 class filestore(object):
480 class filestore(object):
481 def __init__(self):
481 def __init__(self):
482 self.opener = None
482 self.opener = None
483 self.files = {}
483 self.files = {}
484 self.created = 0
484 self.created = 0
485
485
486 def setfile(self, fname, data, mode):
486 def setfile(self, fname, data, mode):
487 if self.opener is None:
487 if self.opener is None:
488 root = tempfile.mkdtemp(prefix='hg-patch-')
488 root = tempfile.mkdtemp(prefix='hg-patch-')
489 self.opener = scmutil.opener(root)
489 self.opener = scmutil.opener(root)
490 # Avoid filename issues with these simple names
490 # Avoid filename issues with these simple names
491 fn = str(self.created)
491 fn = str(self.created)
492 self.opener.write(fn, data)
492 self.opener.write(fn, data)
493 self.created += 1
493 self.created += 1
494 self.files[fname] = (fn, mode)
494 self.files[fname] = (fn, mode)
495
495
496 def getfile(self, fname):
496 def getfile(self, fname):
497 if fname not in self.files:
497 if fname not in self.files:
498 raise IOError()
498 raise IOError()
499 fn, mode = self.files[fname]
499 fn, mode = self.files[fname]
500 return self.opener.read(fn), mode
500 return self.opener.read(fn), mode
501
501
502 def close(self):
502 def close(self):
503 if self.opener:
503 if self.opener:
504 shutil.rmtree(self.opener.base)
504 shutil.rmtree(self.opener.base)
505
505
506 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
506 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
507 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
507 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
508 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
508 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
509 eolmodes = ['strict', 'crlf', 'lf', 'auto']
509 eolmodes = ['strict', 'crlf', 'lf', 'auto']
510
510
511 class patchfile(object):
511 class patchfile(object):
512 def __init__(self, ui, fname, backend, store, mode, create, remove,
512 def __init__(self, ui, fname, backend, store, mode, create, remove,
513 eolmode='strict', copysource=None):
513 eolmode='strict', copysource=None):
514 self.fname = fname
514 self.fname = fname
515 self.eolmode = eolmode
515 self.eolmode = eolmode
516 self.eol = None
516 self.eol = None
517 self.backend = backend
517 self.backend = backend
518 self.ui = ui
518 self.ui = ui
519 self.lines = []
519 self.lines = []
520 self.exists = False
520 self.exists = False
521 self.missing = True
521 self.missing = True
522 self.mode = mode
522 self.mode = mode
523 self.copysource = copysource
523 self.copysource = copysource
524 self.create = create
524 self.create = create
525 self.remove = remove
525 self.remove = remove
526 try:
526 try:
527 if copysource is None:
527 if copysource is None:
528 data, mode = backend.getfile(fname)
528 data, mode = backend.getfile(fname)
529 self.exists = True
529 self.exists = True
530 else:
530 else:
531 data, mode = store.getfile(copysource)
531 data, mode = store.getfile(copysource)
532 self.exists = backend.exists(fname)
532 self.exists = backend.exists(fname)
533 self.missing = False
533 self.missing = False
534 if data:
534 if data:
535 self.lines = data.splitlines(True)
535 self.lines = data.splitlines(True)
536 if self.mode is None:
536 if self.mode is None:
537 self.mode = mode
537 self.mode = mode
538 if self.lines:
538 if self.lines:
539 # Normalize line endings
539 # Normalize line endings
540 if self.lines[0].endswith('\r\n'):
540 if self.lines[0].endswith('\r\n'):
541 self.eol = '\r\n'
541 self.eol = '\r\n'
542 elif self.lines[0].endswith('\n'):
542 elif self.lines[0].endswith('\n'):
543 self.eol = '\n'
543 self.eol = '\n'
544 if eolmode != 'strict':
544 if eolmode != 'strict':
545 nlines = []
545 nlines = []
546 for l in self.lines:
546 for l in self.lines:
547 if l.endswith('\r\n'):
547 if l.endswith('\r\n'):
548 l = l[:-2] + '\n'
548 l = l[:-2] + '\n'
549 nlines.append(l)
549 nlines.append(l)
550 self.lines = nlines
550 self.lines = nlines
551 except IOError:
551 except IOError:
552 if create:
552 if create:
553 self.missing = False
553 self.missing = False
554 if self.mode is None:
554 if self.mode is None:
555 self.mode = (False, False)
555 self.mode = (False, False)
556 if self.missing:
556 if self.missing:
557 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
557 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
558
558
559 self.hash = {}
559 self.hash = {}
560 self.dirty = 0
560 self.dirty = 0
561 self.offset = 0
561 self.offset = 0
562 self.skew = 0
562 self.skew = 0
563 self.rej = []
563 self.rej = []
564 self.fileprinted = False
564 self.fileprinted = False
565 self.printfile(False)
565 self.printfile(False)
566 self.hunks = 0
566 self.hunks = 0
567
567
568 def writelines(self, fname, lines, mode):
568 def writelines(self, fname, lines, mode):
569 if self.eolmode == 'auto':
569 if self.eolmode == 'auto':
570 eol = self.eol
570 eol = self.eol
571 elif self.eolmode == 'crlf':
571 elif self.eolmode == 'crlf':
572 eol = '\r\n'
572 eol = '\r\n'
573 else:
573 else:
574 eol = '\n'
574 eol = '\n'
575
575
576 if self.eolmode != 'strict' and eol and eol != '\n':
576 if self.eolmode != 'strict' and eol and eol != '\n':
577 rawlines = []
577 rawlines = []
578 for l in lines:
578 for l in lines:
579 if l and l[-1] == '\n':
579 if l and l[-1] == '\n':
580 l = l[:-1] + eol
580 l = l[:-1] + eol
581 rawlines.append(l)
581 rawlines.append(l)
582 lines = rawlines
582 lines = rawlines
583
583
584 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
584 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
585
585
586 def printfile(self, warn):
586 def printfile(self, warn):
587 if self.fileprinted:
587 if self.fileprinted:
588 return
588 return
589 if warn or self.ui.verbose:
589 if warn or self.ui.verbose:
590 self.fileprinted = True
590 self.fileprinted = True
591 s = _("patching file %s\n") % self.fname
591 s = _("patching file %s\n") % self.fname
592 if warn:
592 if warn:
593 self.ui.warn(s)
593 self.ui.warn(s)
594 else:
594 else:
595 self.ui.note(s)
595 self.ui.note(s)
596
596
597
597
598 def findlines(self, l, linenum):
598 def findlines(self, l, linenum):
599 # looks through the hash and finds candidate lines. The
599 # looks through the hash and finds candidate lines. The
600 # result is a list of line numbers sorted based on distance
600 # result is a list of line numbers sorted based on distance
601 # from linenum
601 # from linenum
602
602
603 cand = self.hash.get(l, [])
603 cand = self.hash.get(l, [])
604 if len(cand) > 1:
604 if len(cand) > 1:
605 # resort our list of potentials forward then back.
605 # resort our list of potentials forward then back.
606 cand.sort(key=lambda x: abs(x - linenum))
606 cand.sort(key=lambda x: abs(x - linenum))
607 return cand
607 return cand
608
608
609 def write_rej(self):
609 def write_rej(self):
610 # our rejects are a little different from patch(1). This always
610 # our rejects are a little different from patch(1). This always
611 # creates rejects in the same form as the original patch. A file
611 # creates rejects in the same form as the original patch. A file
612 # header is inserted so that you can run the reject through patch again
612 # header is inserted so that you can run the reject through patch again
613 # without having to type the filename.
613 # without having to type the filename.
614 if not self.rej:
614 if not self.rej:
615 return
615 return
616 base = os.path.basename(self.fname)
616 base = os.path.basename(self.fname)
617 lines = ["--- %s\n+++ %s\n" % (base, base)]
617 lines = ["--- %s\n+++ %s\n" % (base, base)]
618 for x in self.rej:
618 for x in self.rej:
619 for l in x.hunk:
619 for l in x.hunk:
620 lines.append(l)
620 lines.append(l)
621 if l[-1] != '\n':
621 if l[-1] != '\n':
622 lines.append("\n\ No newline at end of file\n")
622 lines.append("\n\ No newline at end of file\n")
623 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
623 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
624
624
625 def apply(self, h):
625 def apply(self, h):
626 if not h.complete():
626 if not h.complete():
627 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
627 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
628 (h.number, h.desc, len(h.a), h.lena, len(h.b),
628 (h.number, h.desc, len(h.a), h.lena, len(h.b),
629 h.lenb))
629 h.lenb))
630
630
631 self.hunks += 1
631 self.hunks += 1
632
632
633 if self.missing:
633 if self.missing:
634 self.rej.append(h)
634 self.rej.append(h)
635 return -1
635 return -1
636
636
637 if self.exists and self.create:
637 if self.exists and self.create:
638 if self.copysource:
638 if self.copysource:
639 self.ui.warn(_("cannot create %s: destination already "
639 self.ui.warn(_("cannot create %s: destination already "
640 "exists\n" % self.fname))
640 "exists\n" % self.fname))
641 else:
641 else:
642 self.ui.warn(_("file %s already exists\n") % self.fname)
642 self.ui.warn(_("file %s already exists\n") % self.fname)
643 self.rej.append(h)
643 self.rej.append(h)
644 return -1
644 return -1
645
645
646 if isinstance(h, binhunk):
646 if isinstance(h, binhunk):
647 if self.remove:
647 if self.remove:
648 self.backend.unlink(self.fname)
648 self.backend.unlink(self.fname)
649 else:
649 else:
650 self.lines[:] = h.new()
650 self.lines[:] = h.new()
651 self.offset += len(h.new())
651 self.offset += len(h.new())
652 self.dirty = True
652 self.dirty = True
653 return 0
653 return 0
654
654
655 horig = h
655 horig = h
656 if (self.eolmode in ('crlf', 'lf')
656 if (self.eolmode in ('crlf', 'lf')
657 or self.eolmode == 'auto' and self.eol):
657 or self.eolmode == 'auto' and self.eol):
658 # If new eols are going to be normalized, then normalize
658 # If new eols are going to be normalized, then normalize
659 # hunk data before patching. Otherwise, preserve input
659 # hunk data before patching. Otherwise, preserve input
660 # line-endings.
660 # line-endings.
661 h = h.getnormalized()
661 h = h.getnormalized()
662
662
663 # fast case first, no offsets, no fuzz
663 # fast case first, no offsets, no fuzz
664 old = h.old()
664 old = h.old()
665 # patch starts counting at 1 unless we are adding the file
665 # patch starts counting at 1 unless we are adding the file
666 if h.starta == 0:
666 if h.starta == 0:
667 start = 0
667 start = 0
668 else:
668 else:
669 start = h.starta + self.offset - 1
669 start = h.starta + self.offset - 1
670 orig_start = start
670 orig_start = start
671 # if there's skew we want to emit the "(offset %d lines)" even
671 # if there's skew we want to emit the "(offset %d lines)" even
672 # when the hunk cleanly applies at start + skew, so skip the
672 # when the hunk cleanly applies at start + skew, so skip the
673 # fast case code
673 # fast case code
674 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
674 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
675 if self.remove:
675 if self.remove:
676 self.backend.unlink(self.fname)
676 self.backend.unlink(self.fname)
677 else:
677 else:
678 self.lines[start : start + h.lena] = h.new()
678 self.lines[start : start + h.lena] = h.new()
679 self.offset += h.lenb - h.lena
679 self.offset += h.lenb - h.lena
680 self.dirty = True
680 self.dirty = True
681 return 0
681 return 0
682
682
683 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
683 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
684 self.hash = {}
684 self.hash = {}
685 for x, s in enumerate(self.lines):
685 for x, s in enumerate(self.lines):
686 self.hash.setdefault(s, []).append(x)
686 self.hash.setdefault(s, []).append(x)
687 if h.hunk[-1][0] != ' ':
687 if h.hunk[-1][0] != ' ':
688 # if the hunk tried to put something at the bottom of the file
688 # if the hunk tried to put something at the bottom of the file
689 # override the start line and use eof here
689 # override the start line and use eof here
690 search_start = len(self.lines)
690 search_start = len(self.lines)
691 else:
691 else:
692 search_start = orig_start + self.skew
692 search_start = orig_start + self.skew
693
693
694 for fuzzlen in xrange(3):
694 for fuzzlen in xrange(3):
695 for toponly in [True, False]:
695 for toponly in [True, False]:
696 old = h.old(fuzzlen, toponly)
696 old = h.old(fuzzlen, toponly)
697
697
698 cand = self.findlines(old[0][1:], search_start)
698 cand = self.findlines(old[0][1:], search_start)
699 for l in cand:
699 for l in cand:
700 if diffhelpers.testhunk(old, self.lines, l) == 0:
700 if diffhelpers.testhunk(old, self.lines, l) == 0:
701 newlines = h.new(fuzzlen, toponly)
701 newlines = h.new(fuzzlen, toponly)
702 self.lines[l : l + len(old)] = newlines
702 self.lines[l : l + len(old)] = newlines
703 self.offset += len(newlines) - len(old)
703 self.offset += len(newlines) - len(old)
704 self.skew = l - orig_start
704 self.skew = l - orig_start
705 self.dirty = True
705 self.dirty = True
706 offset = l - orig_start - fuzzlen
706 offset = l - orig_start - fuzzlen
707 if fuzzlen:
707 if fuzzlen:
708 msg = _("Hunk #%d succeeded at %d "
708 msg = _("Hunk #%d succeeded at %d "
709 "with fuzz %d "
709 "with fuzz %d "
710 "(offset %d lines).\n")
710 "(offset %d lines).\n")
711 self.printfile(True)
711 self.printfile(True)
712 self.ui.warn(msg %
712 self.ui.warn(msg %
713 (h.number, l + 1, fuzzlen, offset))
713 (h.number, l + 1, fuzzlen, offset))
714 else:
714 else:
715 msg = _("Hunk #%d succeeded at %d "
715 msg = _("Hunk #%d succeeded at %d "
716 "(offset %d lines).\n")
716 "(offset %d lines).\n")
717 self.ui.note(msg % (h.number, l + 1, offset))
717 self.ui.note(msg % (h.number, l + 1, offset))
718 return fuzzlen
718 return fuzzlen
719 self.printfile(True)
719 self.printfile(True)
720 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
720 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
721 self.rej.append(horig)
721 self.rej.append(horig)
722 return -1
722 return -1
723
723
724 def close(self):
724 def close(self):
725 if self.dirty:
725 if self.dirty:
726 self.writelines(self.fname, self.lines, self.mode)
726 self.writelines(self.fname, self.lines, self.mode)
727 self.write_rej()
727 self.write_rej()
728 return len(self.rej)
728 return len(self.rej)
729
729
730 class hunk(object):
730 class hunk(object):
731 def __init__(self, desc, num, lr, context):
731 def __init__(self, desc, num, lr, context):
732 self.number = num
732 self.number = num
733 self.desc = desc
733 self.desc = desc
734 self.hunk = [desc]
734 self.hunk = [desc]
735 self.a = []
735 self.a = []
736 self.b = []
736 self.b = []
737 self.starta = self.lena = None
737 self.starta = self.lena = None
738 self.startb = self.lenb = None
738 self.startb = self.lenb = None
739 if lr is not None:
739 if lr is not None:
740 if context:
740 if context:
741 self.read_context_hunk(lr)
741 self.read_context_hunk(lr)
742 else:
742 else:
743 self.read_unified_hunk(lr)
743 self.read_unified_hunk(lr)
744
744
745 def getnormalized(self):
745 def getnormalized(self):
746 """Return a copy with line endings normalized to LF."""
746 """Return a copy with line endings normalized to LF."""
747
747
748 def normalize(lines):
748 def normalize(lines):
749 nlines = []
749 nlines = []
750 for line in lines:
750 for line in lines:
751 if line.endswith('\r\n'):
751 if line.endswith('\r\n'):
752 line = line[:-2] + '\n'
752 line = line[:-2] + '\n'
753 nlines.append(line)
753 nlines.append(line)
754 return nlines
754 return nlines
755
755
756 # Dummy object, it is rebuilt manually
756 # Dummy object, it is rebuilt manually
757 nh = hunk(self.desc, self.number, None, None)
757 nh = hunk(self.desc, self.number, None, None)
758 nh.number = self.number
758 nh.number = self.number
759 nh.desc = self.desc
759 nh.desc = self.desc
760 nh.hunk = self.hunk
760 nh.hunk = self.hunk
761 nh.a = normalize(self.a)
761 nh.a = normalize(self.a)
762 nh.b = normalize(self.b)
762 nh.b = normalize(self.b)
763 nh.starta = self.starta
763 nh.starta = self.starta
764 nh.startb = self.startb
764 nh.startb = self.startb
765 nh.lena = self.lena
765 nh.lena = self.lena
766 nh.lenb = self.lenb
766 nh.lenb = self.lenb
767 return nh
767 return nh
768
768
769 def read_unified_hunk(self, lr):
769 def read_unified_hunk(self, lr):
770 m = unidesc.match(self.desc)
770 m = unidesc.match(self.desc)
771 if not m:
771 if not m:
772 raise PatchError(_("bad hunk #%d") % self.number)
772 raise PatchError(_("bad hunk #%d") % self.number)
773 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
773 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
774 if self.lena is None:
774 if self.lena is None:
775 self.lena = 1
775 self.lena = 1
776 else:
776 else:
777 self.lena = int(self.lena)
777 self.lena = int(self.lena)
778 if self.lenb is None:
778 if self.lenb is None:
779 self.lenb = 1
779 self.lenb = 1
780 else:
780 else:
781 self.lenb = int(self.lenb)
781 self.lenb = int(self.lenb)
782 self.starta = int(self.starta)
782 self.starta = int(self.starta)
783 self.startb = int(self.startb)
783 self.startb = int(self.startb)
784 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
784 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
785 # if we hit eof before finishing out the hunk, the last line will
785 # if we hit eof before finishing out the hunk, the last line will
786 # be zero length. Lets try to fix it up.
786 # be zero length. Lets try to fix it up.
787 while len(self.hunk[-1]) == 0:
787 while len(self.hunk[-1]) == 0:
788 del self.hunk[-1]
788 del self.hunk[-1]
789 del self.a[-1]
789 del self.a[-1]
790 del self.b[-1]
790 del self.b[-1]
791 self.lena -= 1
791 self.lena -= 1
792 self.lenb -= 1
792 self.lenb -= 1
793 self._fixnewline(lr)
793 self._fixnewline(lr)
794
794
795 def read_context_hunk(self, lr):
795 def read_context_hunk(self, lr):
796 self.desc = lr.readline()
796 self.desc = lr.readline()
797 m = contextdesc.match(self.desc)
797 m = contextdesc.match(self.desc)
798 if not m:
798 if not m:
799 raise PatchError(_("bad hunk #%d") % self.number)
799 raise PatchError(_("bad hunk #%d") % self.number)
800 foo, self.starta, foo2, aend, foo3 = m.groups()
800 foo, self.starta, foo2, aend, foo3 = m.groups()
801 self.starta = int(self.starta)
801 self.starta = int(self.starta)
802 if aend is None:
802 if aend is None:
803 aend = self.starta
803 aend = self.starta
804 self.lena = int(aend) - self.starta
804 self.lena = int(aend) - self.starta
805 if self.starta:
805 if self.starta:
806 self.lena += 1
806 self.lena += 1
807 for x in xrange(self.lena):
807 for x in xrange(self.lena):
808 l = lr.readline()
808 l = lr.readline()
809 if l.startswith('---'):
809 if l.startswith('---'):
810 # lines addition, old block is empty
810 # lines addition, old block is empty
811 lr.push(l)
811 lr.push(l)
812 break
812 break
813 s = l[2:]
813 s = l[2:]
814 if l.startswith('- ') or l.startswith('! '):
814 if l.startswith('- ') or l.startswith('! '):
815 u = '-' + s
815 u = '-' + s
816 elif l.startswith(' '):
816 elif l.startswith(' '):
817 u = ' ' + s
817 u = ' ' + s
818 else:
818 else:
819 raise PatchError(_("bad hunk #%d old text line %d") %
819 raise PatchError(_("bad hunk #%d old text line %d") %
820 (self.number, x))
820 (self.number, x))
821 self.a.append(u)
821 self.a.append(u)
822 self.hunk.append(u)
822 self.hunk.append(u)
823
823
824 l = lr.readline()
824 l = lr.readline()
825 if l.startswith('\ '):
825 if l.startswith('\ '):
826 s = self.a[-1][:-1]
826 s = self.a[-1][:-1]
827 self.a[-1] = s
827 self.a[-1] = s
828 self.hunk[-1] = s
828 self.hunk[-1] = s
829 l = lr.readline()
829 l = lr.readline()
830 m = contextdesc.match(l)
830 m = contextdesc.match(l)
831 if not m:
831 if not m:
832 raise PatchError(_("bad hunk #%d") % self.number)
832 raise PatchError(_("bad hunk #%d") % self.number)
833 foo, self.startb, foo2, bend, foo3 = m.groups()
833 foo, self.startb, foo2, bend, foo3 = m.groups()
834 self.startb = int(self.startb)
834 self.startb = int(self.startb)
835 if bend is None:
835 if bend is None:
836 bend = self.startb
836 bend = self.startb
837 self.lenb = int(bend) - self.startb
837 self.lenb = int(bend) - self.startb
838 if self.startb:
838 if self.startb:
839 self.lenb += 1
839 self.lenb += 1
840 hunki = 1
840 hunki = 1
841 for x in xrange(self.lenb):
841 for x in xrange(self.lenb):
842 l = lr.readline()
842 l = lr.readline()
843 if l.startswith('\ '):
843 if l.startswith('\ '):
844 # XXX: the only way to hit this is with an invalid line range.
844 # XXX: the only way to hit this is with an invalid line range.
845 # The no-eol marker is not counted in the line range, but I
845 # The no-eol marker is not counted in the line range, but I
846 # guess there are diff(1) out there which behave differently.
846 # guess there are diff(1) out there which behave differently.
847 s = self.b[-1][:-1]
847 s = self.b[-1][:-1]
848 self.b[-1] = s
848 self.b[-1] = s
849 self.hunk[hunki - 1] = s
849 self.hunk[hunki - 1] = s
850 continue
850 continue
851 if not l:
851 if not l:
852 # line deletions, new block is empty and we hit EOF
852 # line deletions, new block is empty and we hit EOF
853 lr.push(l)
853 lr.push(l)
854 break
854 break
855 s = l[2:]
855 s = l[2:]
856 if l.startswith('+ ') or l.startswith('! '):
856 if l.startswith('+ ') or l.startswith('! '):
857 u = '+' + s
857 u = '+' + s
858 elif l.startswith(' '):
858 elif l.startswith(' '):
859 u = ' ' + s
859 u = ' ' + s
860 elif len(self.b) == 0:
860 elif len(self.b) == 0:
861 # line deletions, new block is empty
861 # line deletions, new block is empty
862 lr.push(l)
862 lr.push(l)
863 break
863 break
864 else:
864 else:
865 raise PatchError(_("bad hunk #%d old text line %d") %
865 raise PatchError(_("bad hunk #%d old text line %d") %
866 (self.number, x))
866 (self.number, x))
867 self.b.append(s)
867 self.b.append(s)
868 while True:
868 while True:
869 if hunki >= len(self.hunk):
869 if hunki >= len(self.hunk):
870 h = ""
870 h = ""
871 else:
871 else:
872 h = self.hunk[hunki]
872 h = self.hunk[hunki]
873 hunki += 1
873 hunki += 1
874 if h == u:
874 if h == u:
875 break
875 break
876 elif h.startswith('-'):
876 elif h.startswith('-'):
877 continue
877 continue
878 else:
878 else:
879 self.hunk.insert(hunki - 1, u)
879 self.hunk.insert(hunki - 1, u)
880 break
880 break
881
881
882 if not self.a:
882 if not self.a:
883 # this happens when lines were only added to the hunk
883 # this happens when lines were only added to the hunk
884 for x in self.hunk:
884 for x in self.hunk:
885 if x.startswith('-') or x.startswith(' '):
885 if x.startswith('-') or x.startswith(' '):
886 self.a.append(x)
886 self.a.append(x)
887 if not self.b:
887 if not self.b:
888 # this happens when lines were only deleted from the hunk
888 # this happens when lines were only deleted from the hunk
889 for x in self.hunk:
889 for x in self.hunk:
890 if x.startswith('+') or x.startswith(' '):
890 if x.startswith('+') or x.startswith(' '):
891 self.b.append(x[1:])
891 self.b.append(x[1:])
892 # @@ -start,len +start,len @@
892 # @@ -start,len +start,len @@
893 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
893 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
894 self.startb, self.lenb)
894 self.startb, self.lenb)
895 self.hunk[0] = self.desc
895 self.hunk[0] = self.desc
896 self._fixnewline(lr)
896 self._fixnewline(lr)
897
897
898 def _fixnewline(self, lr):
898 def _fixnewline(self, lr):
899 l = lr.readline()
899 l = lr.readline()
900 if l.startswith('\ '):
900 if l.startswith('\ '):
901 diffhelpers.fix_newline(self.hunk, self.a, self.b)
901 diffhelpers.fix_newline(self.hunk, self.a, self.b)
902 else:
902 else:
903 lr.push(l)
903 lr.push(l)
904
904
905 def complete(self):
905 def complete(self):
906 return len(self.a) == self.lena and len(self.b) == self.lenb
906 return len(self.a) == self.lena and len(self.b) == self.lenb
907
907
908 def fuzzit(self, l, fuzz, toponly):
908 def fuzzit(self, l, fuzz, toponly):
909 # this removes context lines from the top and bottom of list 'l'. It
909 # this removes context lines from the top and bottom of list 'l'. It
910 # checks the hunk to make sure only context lines are removed, and then
910 # checks the hunk to make sure only context lines are removed, and then
911 # returns a new shortened list of lines.
911 # returns a new shortened list of lines.
912 fuzz = min(fuzz, len(l)-1)
912 fuzz = min(fuzz, len(l)-1)
913 if fuzz:
913 if fuzz:
914 top = 0
914 top = 0
915 bot = 0
915 bot = 0
916 hlen = len(self.hunk)
916 hlen = len(self.hunk)
917 for x in xrange(hlen - 1):
917 for x in xrange(hlen - 1):
918 # the hunk starts with the @@ line, so use x+1
918 # the hunk starts with the @@ line, so use x+1
919 if self.hunk[x + 1][0] == ' ':
919 if self.hunk[x + 1][0] == ' ':
920 top += 1
920 top += 1
921 else:
921 else:
922 break
922 break
923 if not toponly:
923 if not toponly:
924 for x in xrange(hlen - 1):
924 for x in xrange(hlen - 1):
925 if self.hunk[hlen - bot - 1][0] == ' ':
925 if self.hunk[hlen - bot - 1][0] == ' ':
926 bot += 1
926 bot += 1
927 else:
927 else:
928 break
928 break
929
929
930 # top and bot now count context in the hunk
930 # top and bot now count context in the hunk
931 # adjust them if either one is short
931 # adjust them if either one is short
932 context = max(top, bot, 3)
932 context = max(top, bot, 3)
933 if bot < context:
933 if bot < context:
934 bot = max(0, fuzz - (context - bot))
934 bot = max(0, fuzz - (context - bot))
935 else:
935 else:
936 bot = min(fuzz, bot)
936 bot = min(fuzz, bot)
937 if top < context:
937 if top < context:
938 top = max(0, fuzz - (context - top))
938 top = max(0, fuzz - (context - top))
939 else:
939 else:
940 top = min(fuzz, top)
940 top = min(fuzz, top)
941
941
942 return l[top:len(l)-bot]
942 return l[top:len(l)-bot]
943 return l
943 return l
944
944
945 def old(self, fuzz=0, toponly=False):
945 def old(self, fuzz=0, toponly=False):
946 return self.fuzzit(self.a, fuzz, toponly)
946 return self.fuzzit(self.a, fuzz, toponly)
947
947
948 def new(self, fuzz=0, toponly=False):
948 def new(self, fuzz=0, toponly=False):
949 return self.fuzzit(self.b, fuzz, toponly)
949 return self.fuzzit(self.b, fuzz, toponly)
950
950
951 class binhunk:
951 class binhunk:
952 'A binary patch file. Only understands literals so far.'
952 'A binary patch file. Only understands literals so far.'
953 def __init__(self, lr):
953 def __init__(self, lr):
954 self.text = None
954 self.text = None
955 self.hunk = ['GIT binary patch\n']
955 self.hunk = ['GIT binary patch\n']
956 self._read(lr)
956 self._read(lr)
957
957
958 def complete(self):
958 def complete(self):
959 return self.text is not None
959 return self.text is not None
960
960
961 def new(self):
961 def new(self):
962 return [self.text]
962 return [self.text]
963
963
964 def _read(self, lr):
964 def _read(self, lr):
965 line = lr.readline()
965 line = lr.readline()
966 self.hunk.append(line)
966 self.hunk.append(line)
967 while line and not line.startswith('literal '):
967 while line and not line.startswith('literal '):
968 line = lr.readline()
968 line = lr.readline()
969 self.hunk.append(line)
969 self.hunk.append(line)
970 if not line:
970 if not line:
971 raise PatchError(_('could not extract binary patch'))
971 raise PatchError(_('could not extract binary patch'))
972 size = int(line[8:].rstrip())
972 size = int(line[8:].rstrip())
973 dec = []
973 dec = []
974 line = lr.readline()
974 line = lr.readline()
975 self.hunk.append(line)
975 self.hunk.append(line)
976 while len(line) > 1:
976 while len(line) > 1:
977 l = line[0]
977 l = line[0]
978 if l <= 'Z' and l >= 'A':
978 if l <= 'Z' and l >= 'A':
979 l = ord(l) - ord('A') + 1
979 l = ord(l) - ord('A') + 1
980 else:
980 else:
981 l = ord(l) - ord('a') + 27
981 l = ord(l) - ord('a') + 27
982 dec.append(base85.b85decode(line[1:-1])[:l])
982 dec.append(base85.b85decode(line[1:-1])[:l])
983 line = lr.readline()
983 line = lr.readline()
984 self.hunk.append(line)
984 self.hunk.append(line)
985 text = zlib.decompress(''.join(dec))
985 text = zlib.decompress(''.join(dec))
986 if len(text) != size:
986 if len(text) != size:
987 raise PatchError(_('binary patch is %d bytes, not %d') %
987 raise PatchError(_('binary patch is %d bytes, not %d') %
988 len(text), size)
988 len(text), size)
989 self.text = text
989 self.text = text
990
990
991 def parsefilename(str):
991 def parsefilename(str):
992 # --- filename \t|space stuff
992 # --- filename \t|space stuff
993 s = str[4:].rstrip('\r\n')
993 s = str[4:].rstrip('\r\n')
994 i = s.find('\t')
994 i = s.find('\t')
995 if i < 0:
995 if i < 0:
996 i = s.find(' ')
996 i = s.find(' ')
997 if i < 0:
997 if i < 0:
998 return s
998 return s
999 return s[:i]
999 return s[:i]
1000
1000
1001 def pathstrip(path, strip):
1001 def pathstrip(path, strip):
1002 pathlen = len(path)
1002 pathlen = len(path)
1003 i = 0
1003 i = 0
1004 if strip == 0:
1004 if strip == 0:
1005 return '', path.rstrip()
1005 return '', path.rstrip()
1006 count = strip
1006 count = strip
1007 while count > 0:
1007 while count > 0:
1008 i = path.find('/', i)
1008 i = path.find('/', i)
1009 if i == -1:
1009 if i == -1:
1010 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1010 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1011 (count, strip, path))
1011 (count, strip, path))
1012 i += 1
1012 i += 1
1013 # consume '//' in the path
1013 # consume '//' in the path
1014 while i < pathlen - 1 and path[i] == '/':
1014 while i < pathlen - 1 and path[i] == '/':
1015 i += 1
1015 i += 1
1016 count -= 1
1016 count -= 1
1017 return path[:i].lstrip(), path[i:].rstrip()
1017 return path[:i].lstrip(), path[i:].rstrip()
1018
1018
1019 def selectfile(backend, afile_orig, bfile_orig, hunk, strip, gp):
1019 def selectfile(backend, afile_orig, bfile_orig, hunk, strip, gp):
1020 if gp:
1020 if gp:
1021 # Git patches do not play games. Excluding copies from the
1021 # Git patches do not play games. Excluding copies from the
1022 # following heuristic avoids a lot of confusion
1022 # following heuristic avoids a lot of confusion
1023 fname = pathstrip(gp.path, strip - 1)[1]
1023 fname = pathstrip(gp.path, strip - 1)[1]
1024 create = gp.op in ('ADD', 'COPY', 'RENAME')
1024 create = gp.op in ('ADD', 'COPY', 'RENAME')
1025 remove = gp.op == 'DELETE'
1025 remove = gp.op == 'DELETE'
1026 return fname, create, remove
1026 return fname, create, remove
1027 nulla = afile_orig == "/dev/null"
1027 nulla = afile_orig == "/dev/null"
1028 nullb = bfile_orig == "/dev/null"
1028 nullb = bfile_orig == "/dev/null"
1029 create = nulla and hunk.starta == 0 and hunk.lena == 0
1029 create = nulla and hunk.starta == 0 and hunk.lena == 0
1030 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1030 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1031 abase, afile = pathstrip(afile_orig, strip)
1031 abase, afile = pathstrip(afile_orig, strip)
1032 gooda = not nulla and backend.exists(afile)
1032 gooda = not nulla and backend.exists(afile)
1033 bbase, bfile = pathstrip(bfile_orig, strip)
1033 bbase, bfile = pathstrip(bfile_orig, strip)
1034 if afile == bfile:
1034 if afile == bfile:
1035 goodb = gooda
1035 goodb = gooda
1036 else:
1036 else:
1037 goodb = not nullb and backend.exists(bfile)
1037 goodb = not nullb and backend.exists(bfile)
1038 missing = not goodb and not gooda and not create
1038 missing = not goodb and not gooda and not create
1039
1039
1040 # some diff programs apparently produce patches where the afile is
1040 # some diff programs apparently produce patches where the afile is
1041 # not /dev/null, but afile starts with bfile
1041 # not /dev/null, but afile starts with bfile
1042 abasedir = afile[:afile.rfind('/') + 1]
1042 abasedir = afile[:afile.rfind('/') + 1]
1043 bbasedir = bfile[:bfile.rfind('/') + 1]
1043 bbasedir = bfile[:bfile.rfind('/') + 1]
1044 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1044 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1045 and hunk.starta == 0 and hunk.lena == 0):
1045 and hunk.starta == 0 and hunk.lena == 0):
1046 create = True
1046 create = True
1047 missing = False
1047 missing = False
1048
1048
1049 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1049 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1050 # diff is between a file and its backup. In this case, the original
1050 # diff is between a file and its backup. In this case, the original
1051 # file should be patched (see original mpatch code).
1051 # file should be patched (see original mpatch code).
1052 isbackup = (abase == bbase and bfile.startswith(afile))
1052 isbackup = (abase == bbase and bfile.startswith(afile))
1053 fname = None
1053 fname = None
1054 if not missing:
1054 if not missing:
1055 if gooda and goodb:
1055 if gooda and goodb:
1056 fname = isbackup and afile or bfile
1056 fname = isbackup and afile or bfile
1057 elif gooda:
1057 elif gooda:
1058 fname = afile
1058 fname = afile
1059
1059
1060 if not fname:
1060 if not fname:
1061 if not nullb:
1061 if not nullb:
1062 fname = isbackup and afile or bfile
1062 fname = isbackup and afile or bfile
1063 elif not nulla:
1063 elif not nulla:
1064 fname = afile
1064 fname = afile
1065 else:
1065 else:
1066 raise PatchError(_("undefined source and destination files"))
1066 raise PatchError(_("undefined source and destination files"))
1067
1067
1068 return fname, create, remove
1068 return fname, create, remove
1069
1069
1070 def scangitpatch(lr, firstline):
1070 def scangitpatch(lr, firstline):
1071 """
1071 """
1072 Git patches can emit:
1072 Git patches can emit:
1073 - rename a to b
1073 - rename a to b
1074 - change b
1074 - change b
1075 - copy a to c
1075 - copy a to c
1076 - change c
1076 - change c
1077
1077
1078 We cannot apply this sequence as-is, the renamed 'a' could not be
1078 We cannot apply this sequence as-is, the renamed 'a' could not be
1079 found for it would have been renamed already. And we cannot copy
1079 found for it would have been renamed already. And we cannot copy
1080 from 'b' instead because 'b' would have been changed already. So
1080 from 'b' instead because 'b' would have been changed already. So
1081 we scan the git patch for copy and rename commands so we can
1081 we scan the git patch for copy and rename commands so we can
1082 perform the copies ahead of time.
1082 perform the copies ahead of time.
1083 """
1083 """
1084 pos = 0
1084 pos = 0
1085 try:
1085 try:
1086 pos = lr.fp.tell()
1086 pos = lr.fp.tell()
1087 fp = lr.fp
1087 fp = lr.fp
1088 except IOError:
1088 except IOError:
1089 fp = cStringIO.StringIO(lr.fp.read())
1089 fp = cStringIO.StringIO(lr.fp.read())
1090 gitlr = linereader(fp)
1090 gitlr = linereader(fp)
1091 gitlr.push(firstline)
1091 gitlr.push(firstline)
1092 gitpatches = readgitpatch(gitlr)
1092 gitpatches = readgitpatch(gitlr)
1093 fp.seek(pos)
1093 fp.seek(pos)
1094 return gitpatches
1094 return gitpatches
1095
1095
1096 def iterhunks(fp):
1096 def iterhunks(fp):
1097 """Read a patch and yield the following events:
1097 """Read a patch and yield the following events:
1098 - ("file", afile, bfile, firsthunk): select a new target file.
1098 - ("file", afile, bfile, firsthunk): select a new target file.
1099 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1099 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1100 "file" event.
1100 "file" event.
1101 - ("git", gitchanges): current diff is in git format, gitchanges
1101 - ("git", gitchanges): current diff is in git format, gitchanges
1102 maps filenames to gitpatch records. Unique event.
1102 maps filenames to gitpatch records. Unique event.
1103 """
1103 """
1104 afile = ""
1104 afile = ""
1105 bfile = ""
1105 bfile = ""
1106 state = None
1106 state = None
1107 hunknum = 0
1107 hunknum = 0
1108 emitfile = newfile = False
1108 emitfile = newfile = False
1109 gitpatches = None
1109 gitpatches = None
1110
1110
1111 # our states
1111 # our states
1112 BFILE = 1
1112 BFILE = 1
1113 context = None
1113 context = None
1114 lr = linereader(fp)
1114 lr = linereader(fp)
1115
1115
1116 while True:
1116 while True:
1117 x = lr.readline()
1117 x = lr.readline()
1118 if not x:
1118 if not x:
1119 break
1119 break
1120 if state == BFILE and (
1120 if state == BFILE and (
1121 (not context and x[0] == '@')
1121 (not context and x[0] == '@')
1122 or (context is not False and x.startswith('***************'))
1122 or (context is not False and x.startswith('***************'))
1123 or x.startswith('GIT binary patch')):
1123 or x.startswith('GIT binary patch')):
1124 gp = None
1124 gp = None
1125 if (gitpatches and
1125 if (gitpatches and
1126 (gitpatches[-1][0] == afile or gitpatches[-1][1] == bfile)):
1126 (gitpatches[-1][0] == afile or gitpatches[-1][1] == bfile)):
1127 gp = gitpatches.pop()[2]
1127 gp = gitpatches.pop()[2]
1128 if x.startswith('GIT binary patch'):
1128 if x.startswith('GIT binary patch'):
1129 h = binhunk(lr)
1129 h = binhunk(lr)
1130 else:
1130 else:
1131 if context is None and x.startswith('***************'):
1131 if context is None and x.startswith('***************'):
1132 context = True
1132 context = True
1133 h = hunk(x, hunknum + 1, lr, context)
1133 h = hunk(x, hunknum + 1, lr, context)
1134 hunknum += 1
1134 hunknum += 1
1135 if emitfile:
1135 if emitfile:
1136 emitfile = False
1136 emitfile = False
1137 yield 'file', (afile, bfile, h, gp)
1137 yield 'file', (afile, bfile, h, gp)
1138 yield 'hunk', h
1138 yield 'hunk', h
1139 elif x.startswith('diff --git'):
1139 elif x.startswith('diff --git'):
1140 m = gitre.match(x)
1140 m = gitre.match(x)
1141 if not m:
1141 if not m:
1142 continue
1142 continue
1143 if gitpatches is None:
1143 if gitpatches is None:
1144 # scan whole input for git metadata
1144 # scan whole input for git metadata
1145 gitpatches = [('a/' + gp.path, 'b/' + gp.path, gp) for gp
1145 gitpatches = [('a/' + gp.path, 'b/' + gp.path, gp) for gp
1146 in scangitpatch(lr, x)]
1146 in scangitpatch(lr, x)]
1147 yield 'git', [g[2] for g in gitpatches
1147 yield 'git', [g[2] for g in gitpatches
1148 if g[2].op in ('COPY', 'RENAME')]
1148 if g[2].op in ('COPY', 'RENAME')]
1149 gitpatches.reverse()
1149 gitpatches.reverse()
1150 afile = 'a/' + m.group(1)
1150 afile = 'a/' + m.group(1)
1151 bfile = 'b/' + m.group(2)
1151 bfile = 'b/' + m.group(2)
1152 while afile != gitpatches[-1][0] and bfile != gitpatches[-1][1]:
1152 while afile != gitpatches[-1][0] and bfile != gitpatches[-1][1]:
1153 gp = gitpatches.pop()[2]
1153 gp = gitpatches.pop()[2]
1154 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp)
1154 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp)
1155 gp = gitpatches[-1][2]
1155 gp = gitpatches[-1][2]
1156 # copy/rename + modify should modify target, not source
1156 # copy/rename + modify should modify target, not source
1157 if gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD') or gp.mode:
1157 if gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD') or gp.mode:
1158 afile = bfile
1158 afile = bfile
1159 newfile = True
1159 newfile = True
1160 elif x.startswith('---'):
1160 elif x.startswith('---'):
1161 # check for a unified diff
1161 # check for a unified diff
1162 l2 = lr.readline()
1162 l2 = lr.readline()
1163 if not l2.startswith('+++'):
1163 if not l2.startswith('+++'):
1164 lr.push(l2)
1164 lr.push(l2)
1165 continue
1165 continue
1166 newfile = True
1166 newfile = True
1167 context = False
1167 context = False
1168 afile = parsefilename(x)
1168 afile = parsefilename(x)
1169 bfile = parsefilename(l2)
1169 bfile = parsefilename(l2)
1170 elif x.startswith('***'):
1170 elif x.startswith('***'):
1171 # check for a context diff
1171 # check for a context diff
1172 l2 = lr.readline()
1172 l2 = lr.readline()
1173 if not l2.startswith('---'):
1173 if not l2.startswith('---'):
1174 lr.push(l2)
1174 lr.push(l2)
1175 continue
1175 continue
1176 l3 = lr.readline()
1176 l3 = lr.readline()
1177 lr.push(l3)
1177 lr.push(l3)
1178 if not l3.startswith("***************"):
1178 if not l3.startswith("***************"):
1179 lr.push(l2)
1179 lr.push(l2)
1180 continue
1180 continue
1181 newfile = True
1181 newfile = True
1182 context = True
1182 context = True
1183 afile = parsefilename(x)
1183 afile = parsefilename(x)
1184 bfile = parsefilename(l2)
1184 bfile = parsefilename(l2)
1185
1185
1186 if newfile:
1186 if newfile:
1187 newfile = False
1187 newfile = False
1188 emitfile = True
1188 emitfile = True
1189 state = BFILE
1189 state = BFILE
1190 hunknum = 0
1190 hunknum = 0
1191
1191
1192 while gitpatches:
1192 while gitpatches:
1193 gp = gitpatches.pop()[2]
1193 gp = gitpatches.pop()[2]
1194 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp)
1194 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp)
1195
1195
1196 def applydiff(ui, fp, changed, backend, store, strip=1, eolmode='strict'):
1196 def applydiff(ui, fp, backend, store, strip=1, eolmode='strict'):
1197 """Reads a patch from fp and tries to apply it.
1197 """Reads a patch from fp and tries to apply it.
1198
1198
1199 The dict 'changed' is filled in with all of the filenames changed
1199 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1200 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1200 there was any fuzz.
1201 found and 1 if there was any fuzz.
1202
1201
1203 If 'eolmode' is 'strict', the patch content and patched file are
1202 If 'eolmode' is 'strict', the patch content and patched file are
1204 read in binary mode. Otherwise, line endings are ignored when
1203 read in binary mode. Otherwise, line endings are ignored when
1205 patching then normalized according to 'eolmode'.
1204 patching then normalized according to 'eolmode'.
1206 """
1205 """
1207 return _applydiff(ui, fp, patchfile, backend, store, changed, strip=strip,
1206 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1208 eolmode=eolmode)
1207 eolmode=eolmode)
1209
1208
1210 def _applydiff(ui, fp, patcher, backend, store, changed, strip=1,
1209 def _applydiff(ui, fp, patcher, backend, store, strip=1,
1211 eolmode='strict'):
1210 eolmode='strict'):
1212
1211
1213 def pstrip(p):
1212 def pstrip(p):
1214 return pathstrip(p, strip - 1)[1]
1213 return pathstrip(p, strip - 1)[1]
1215
1214
1216 rejects = 0
1215 rejects = 0
1217 err = 0
1216 err = 0
1218 current_file = None
1217 current_file = None
1219
1218
1220 for state, values in iterhunks(fp):
1219 for state, values in iterhunks(fp):
1221 if state == 'hunk':
1220 if state == 'hunk':
1222 if not current_file:
1221 if not current_file:
1223 continue
1222 continue
1224 ret = current_file.apply(values)
1223 ret = current_file.apply(values)
1225 if ret >= 0:
1224 if ret > 0:
1226 changed.add(current_file.fname)
1225 err = 1
1227 if ret > 0:
1228 err = 1
1229 elif state == 'file':
1226 elif state == 'file':
1230 if current_file:
1227 if current_file:
1231 rejects += current_file.close()
1228 rejects += current_file.close()
1232 current_file = None
1229 current_file = None
1233 afile, bfile, first_hunk, gp = values
1230 afile, bfile, first_hunk, gp = values
1234 copysource = None
1231 copysource = None
1235 if gp:
1232 if gp:
1236 path = pstrip(gp.path)
1233 path = pstrip(gp.path)
1237 if gp.oldpath:
1234 if gp.oldpath:
1238 copysource = pstrip(gp.oldpath)
1235 copysource = pstrip(gp.oldpath)
1239 changed.add(path)
1240 if gp.op == 'RENAME':
1236 if gp.op == 'RENAME':
1241 backend.unlink(copysource)
1237 backend.unlink(copysource)
1242 if not first_hunk:
1238 if not first_hunk:
1243 if gp.op == 'DELETE':
1239 if gp.op == 'DELETE':
1244 backend.unlink(path)
1240 backend.unlink(path)
1245 continue
1241 continue
1246 data, mode = None, None
1242 data, mode = None, None
1247 if gp.op in ('RENAME', 'COPY'):
1243 if gp.op in ('RENAME', 'COPY'):
1248 data, mode = store.getfile(copysource)
1244 data, mode = store.getfile(copysource)
1249 if gp.mode:
1245 if gp.mode:
1250 mode = gp.mode
1246 mode = gp.mode
1251 if gp.op == 'ADD':
1247 if gp.op == 'ADD':
1252 # Added files without content have no hunk and
1248 # Added files without content have no hunk and
1253 # must be created
1249 # must be created
1254 data = ''
1250 data = ''
1255 if data or mode:
1251 if data or mode:
1256 if (gp.op in ('ADD', 'RENAME', 'COPY')
1252 if (gp.op in ('ADD', 'RENAME', 'COPY')
1257 and backend.exists(path)):
1253 and backend.exists(path)):
1258 raise PatchError(_("cannot create %s: destination "
1254 raise PatchError(_("cannot create %s: destination "
1259 "already exists") % path)
1255 "already exists") % path)
1260 backend.setfile(path, data, mode, copysource)
1256 backend.setfile(path, data, mode, copysource)
1261 if not first_hunk:
1257 if not first_hunk:
1262 continue
1258 continue
1263 try:
1259 try:
1264 mode = gp and gp.mode or None
1260 mode = gp and gp.mode or None
1265 current_file, create, remove = selectfile(
1261 current_file, create, remove = selectfile(
1266 backend, afile, bfile, first_hunk, strip, gp)
1262 backend, afile, bfile, first_hunk, strip, gp)
1267 current_file = patcher(ui, current_file, backend, store, mode,
1263 current_file = patcher(ui, current_file, backend, store, mode,
1268 create, remove, eolmode=eolmode,
1264 create, remove, eolmode=eolmode,
1269 copysource=copysource)
1265 copysource=copysource)
1270 except PatchError, inst:
1266 except PatchError, inst:
1271 ui.warn(str(inst) + '\n')
1267 ui.warn(str(inst) + '\n')
1272 current_file = None
1268 current_file = None
1273 rejects += 1
1269 rejects += 1
1274 continue
1270 continue
1275 elif state == 'git':
1271 elif state == 'git':
1276 for gp in values:
1272 for gp in values:
1277 path = pstrip(gp.oldpath)
1273 path = pstrip(gp.oldpath)
1278 data, mode = backend.getfile(path)
1274 data, mode = backend.getfile(path)
1279 store.setfile(path, data, mode)
1275 store.setfile(path, data, mode)
1280 else:
1276 else:
1281 raise util.Abort(_('unsupported parser state: %s') % state)
1277 raise util.Abort(_('unsupported parser state: %s') % state)
1282
1278
1283 if current_file:
1279 if current_file:
1284 rejects += current_file.close()
1280 rejects += current_file.close()
1285
1281
1286 if rejects:
1282 if rejects:
1287 return -1
1283 return -1
1288 return err
1284 return err
1289
1285
1290 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1286 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1291 similarity):
1287 similarity):
1292 """use <patcher> to apply <patchname> to the working directory.
1288 """use <patcher> to apply <patchname> to the working directory.
1293 returns whether patch was applied with fuzz factor."""
1289 returns whether patch was applied with fuzz factor."""
1294
1290
1295 fuzz = False
1291 fuzz = False
1296 args = []
1292 args = []
1297 cwd = repo.root
1293 cwd = repo.root
1298 if cwd:
1294 if cwd:
1299 args.append('-d %s' % util.shellquote(cwd))
1295 args.append('-d %s' % util.shellquote(cwd))
1300 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1296 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1301 util.shellquote(patchname)))
1297 util.shellquote(patchname)))
1302 try:
1298 try:
1303 for line in fp:
1299 for line in fp:
1304 line = line.rstrip()
1300 line = line.rstrip()
1305 ui.note(line + '\n')
1301 ui.note(line + '\n')
1306 if line.startswith('patching file '):
1302 if line.startswith('patching file '):
1307 pf = util.parsepatchoutput(line)
1303 pf = util.parsepatchoutput(line)
1308 printed_file = False
1304 printed_file = False
1309 files.add(pf)
1305 files.add(pf)
1310 elif line.find('with fuzz') >= 0:
1306 elif line.find('with fuzz') >= 0:
1311 fuzz = True
1307 fuzz = True
1312 if not printed_file:
1308 if not printed_file:
1313 ui.warn(pf + '\n')
1309 ui.warn(pf + '\n')
1314 printed_file = True
1310 printed_file = True
1315 ui.warn(line + '\n')
1311 ui.warn(line + '\n')
1316 elif line.find('saving rejects to file') >= 0:
1312 elif line.find('saving rejects to file') >= 0:
1317 ui.warn(line + '\n')
1313 ui.warn(line + '\n')
1318 elif line.find('FAILED') >= 0:
1314 elif line.find('FAILED') >= 0:
1319 if not printed_file:
1315 if not printed_file:
1320 ui.warn(pf + '\n')
1316 ui.warn(pf + '\n')
1321 printed_file = True
1317 printed_file = True
1322 ui.warn(line + '\n')
1318 ui.warn(line + '\n')
1323 finally:
1319 finally:
1324 if files:
1320 if files:
1325 cfiles = list(files)
1321 cfiles = list(files)
1326 cwd = repo.getcwd()
1322 cwd = repo.getcwd()
1327 if cwd:
1323 if cwd:
1328 cfiles = [util.pathto(repo.root, cwd, f)
1324 cfiles = [util.pathto(repo.root, cwd, f)
1329 for f in cfile]
1325 for f in cfile]
1330 scmutil.addremove(repo, cfiles, similarity=similarity)
1326 scmutil.addremove(repo, cfiles, similarity=similarity)
1331 code = fp.close()
1327 code = fp.close()
1332 if code:
1328 if code:
1333 raise PatchError(_("patch command failed: %s") %
1329 raise PatchError(_("patch command failed: %s") %
1334 util.explainexit(code)[0])
1330 util.explainexit(code)[0])
1335 return fuzz
1331 return fuzz
1336
1332
1337 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1333 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1338 similarity=0):
1334 similarity=0):
1339 """use builtin patch to apply <patchobj> to the working directory.
1335 """use builtin patch to apply <patchobj> to the working directory.
1340 returns whether patch was applied with fuzz factor."""
1336 returns whether patch was applied with fuzz factor."""
1341
1337
1342 if files is None:
1338 if files is None:
1343 files = set()
1339 files = set()
1344 if eolmode is None:
1340 if eolmode is None:
1345 eolmode = ui.config('patch', 'eol', 'strict')
1341 eolmode = ui.config('patch', 'eol', 'strict')
1346 if eolmode.lower() not in eolmodes:
1342 if eolmode.lower() not in eolmodes:
1347 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1343 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1348 eolmode = eolmode.lower()
1344 eolmode = eolmode.lower()
1349
1345
1350 store = filestore()
1346 store = filestore()
1351 backend = workingbackend(ui, repo, similarity)
1347 backend = workingbackend(ui, repo, similarity)
1352 try:
1348 try:
1353 fp = open(patchobj, 'rb')
1349 fp = open(patchobj, 'rb')
1354 except TypeError:
1350 except TypeError:
1355 fp = patchobj
1351 fp = patchobj
1356 try:
1352 try:
1357 ret = applydiff(ui, fp, files, backend, store, strip=strip,
1353 ret = applydiff(ui, fp, backend, store, strip=strip,
1358 eolmode=eolmode)
1354 eolmode=eolmode)
1359 finally:
1355 finally:
1360 if fp != patchobj:
1356 if fp != patchobj:
1361 fp.close()
1357 fp.close()
1362 files.update(backend.close())
1358 files.update(backend.close())
1363 store.close()
1359 store.close()
1364 if ret < 0:
1360 if ret < 0:
1365 raise PatchError(_('patch failed to apply'))
1361 raise PatchError(_('patch failed to apply'))
1366 return ret > 0
1362 return ret > 0
1367
1363
1368 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1364 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1369 similarity=0):
1365 similarity=0):
1370 """Apply <patchname> to the working directory.
1366 """Apply <patchname> to the working directory.
1371
1367
1372 'eolmode' specifies how end of lines should be handled. It can be:
1368 'eolmode' specifies how end of lines should be handled. It can be:
1373 - 'strict': inputs are read in binary mode, EOLs are preserved
1369 - 'strict': inputs are read in binary mode, EOLs are preserved
1374 - 'crlf': EOLs are ignored when patching and reset to CRLF
1370 - 'crlf': EOLs are ignored when patching and reset to CRLF
1375 - 'lf': EOLs are ignored when patching and reset to LF
1371 - 'lf': EOLs are ignored when patching and reset to LF
1376 - None: get it from user settings, default to 'strict'
1372 - None: get it from user settings, default to 'strict'
1377 'eolmode' is ignored when using an external patcher program.
1373 'eolmode' is ignored when using an external patcher program.
1378
1374
1379 Returns whether patch was applied with fuzz factor.
1375 Returns whether patch was applied with fuzz factor.
1380 """
1376 """
1381 patcher = ui.config('ui', 'patch')
1377 patcher = ui.config('ui', 'patch')
1382 if files is None:
1378 if files is None:
1383 files = set()
1379 files = set()
1384 try:
1380 try:
1385 if patcher:
1381 if patcher:
1386 return _externalpatch(ui, repo, patcher, patchname, strip,
1382 return _externalpatch(ui, repo, patcher, patchname, strip,
1387 files, similarity)
1383 files, similarity)
1388 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1384 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1389 similarity)
1385 similarity)
1390 except PatchError, err:
1386 except PatchError, err:
1391 raise util.Abort(str(err))
1387 raise util.Abort(str(err))
1392
1388
1393 def changedfiles(ui, repo, patchpath, strip=1):
1389 def changedfiles(ui, repo, patchpath, strip=1):
1394 backend = fsbackend(ui, repo.root)
1390 backend = fsbackend(ui, repo.root)
1395 fp = open(patchpath, 'rb')
1391 fp = open(patchpath, 'rb')
1396 try:
1392 try:
1397 changed = set()
1393 changed = set()
1398 for state, values in iterhunks(fp):
1394 for state, values in iterhunks(fp):
1399 if state == 'file':
1395 if state == 'file':
1400 afile, bfile, first_hunk, gp = values
1396 afile, bfile, first_hunk, gp = values
1401 if gp:
1397 if gp:
1402 changed.add(pathstrip(gp.path, strip - 1)[1])
1398 changed.add(pathstrip(gp.path, strip - 1)[1])
1403 if gp.op == 'RENAME':
1399 if gp.op == 'RENAME':
1404 changed.add(pathstrip(gp.oldpath, strip - 1)[1])
1400 changed.add(pathstrip(gp.oldpath, strip - 1)[1])
1405 if not first_hunk:
1401 if not first_hunk:
1406 continue
1402 continue
1407 current_file, create, remove = selectfile(
1403 current_file, create, remove = selectfile(
1408 backend, afile, bfile, first_hunk, strip, gp)
1404 backend, afile, bfile, first_hunk, strip, gp)
1409 changed.add(current_file)
1405 changed.add(current_file)
1410 elif state not in ('hunk', 'git'):
1406 elif state not in ('hunk', 'git'):
1411 raise util.Abort(_('unsupported parser state: %s') % state)
1407 raise util.Abort(_('unsupported parser state: %s') % state)
1412 return changed
1408 return changed
1413 finally:
1409 finally:
1414 fp.close()
1410 fp.close()
1415
1411
1416 def b85diff(to, tn):
1412 def b85diff(to, tn):
1417 '''print base85-encoded binary diff'''
1413 '''print base85-encoded binary diff'''
1418 def gitindex(text):
1414 def gitindex(text):
1419 if not text:
1415 if not text:
1420 return hex(nullid)
1416 return hex(nullid)
1421 l = len(text)
1417 l = len(text)
1422 s = util.sha1('blob %d\0' % l)
1418 s = util.sha1('blob %d\0' % l)
1423 s.update(text)
1419 s.update(text)
1424 return s.hexdigest()
1420 return s.hexdigest()
1425
1421
1426 def fmtline(line):
1422 def fmtline(line):
1427 l = len(line)
1423 l = len(line)
1428 if l <= 26:
1424 if l <= 26:
1429 l = chr(ord('A') + l - 1)
1425 l = chr(ord('A') + l - 1)
1430 else:
1426 else:
1431 l = chr(l - 26 + ord('a') - 1)
1427 l = chr(l - 26 + ord('a') - 1)
1432 return '%c%s\n' % (l, base85.b85encode(line, True))
1428 return '%c%s\n' % (l, base85.b85encode(line, True))
1433
1429
1434 def chunk(text, csize=52):
1430 def chunk(text, csize=52):
1435 l = len(text)
1431 l = len(text)
1436 i = 0
1432 i = 0
1437 while i < l:
1433 while i < l:
1438 yield text[i:i + csize]
1434 yield text[i:i + csize]
1439 i += csize
1435 i += csize
1440
1436
1441 tohash = gitindex(to)
1437 tohash = gitindex(to)
1442 tnhash = gitindex(tn)
1438 tnhash = gitindex(tn)
1443 if tohash == tnhash:
1439 if tohash == tnhash:
1444 return ""
1440 return ""
1445
1441
1446 # TODO: deltas
1442 # TODO: deltas
1447 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1443 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1448 (tohash, tnhash, len(tn))]
1444 (tohash, tnhash, len(tn))]
1449 for l in chunk(zlib.compress(tn)):
1445 for l in chunk(zlib.compress(tn)):
1450 ret.append(fmtline(l))
1446 ret.append(fmtline(l))
1451 ret.append('\n')
1447 ret.append('\n')
1452 return ''.join(ret)
1448 return ''.join(ret)
1453
1449
1454 class GitDiffRequired(Exception):
1450 class GitDiffRequired(Exception):
1455 pass
1451 pass
1456
1452
1457 def diffopts(ui, opts=None, untrusted=False):
1453 def diffopts(ui, opts=None, untrusted=False):
1458 def get(key, name=None, getter=ui.configbool):
1454 def get(key, name=None, getter=ui.configbool):
1459 return ((opts and opts.get(key)) or
1455 return ((opts and opts.get(key)) or
1460 getter('diff', name or key, None, untrusted=untrusted))
1456 getter('diff', name or key, None, untrusted=untrusted))
1461 return mdiff.diffopts(
1457 return mdiff.diffopts(
1462 text=opts and opts.get('text'),
1458 text=opts and opts.get('text'),
1463 git=get('git'),
1459 git=get('git'),
1464 nodates=get('nodates'),
1460 nodates=get('nodates'),
1465 showfunc=get('show_function', 'showfunc'),
1461 showfunc=get('show_function', 'showfunc'),
1466 ignorews=get('ignore_all_space', 'ignorews'),
1462 ignorews=get('ignore_all_space', 'ignorews'),
1467 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1463 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1468 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1464 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1469 context=get('unified', getter=ui.config))
1465 context=get('unified', getter=ui.config))
1470
1466
1471 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1467 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1472 losedatafn=None, prefix=''):
1468 losedatafn=None, prefix=''):
1473 '''yields diff of changes to files between two nodes, or node and
1469 '''yields diff of changes to files between two nodes, or node and
1474 working directory.
1470 working directory.
1475
1471
1476 if node1 is None, use first dirstate parent instead.
1472 if node1 is None, use first dirstate parent instead.
1477 if node2 is None, compare node1 with working directory.
1473 if node2 is None, compare node1 with working directory.
1478
1474
1479 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1475 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1480 every time some change cannot be represented with the current
1476 every time some change cannot be represented with the current
1481 patch format. Return False to upgrade to git patch format, True to
1477 patch format. Return False to upgrade to git patch format, True to
1482 accept the loss or raise an exception to abort the diff. It is
1478 accept the loss or raise an exception to abort the diff. It is
1483 called with the name of current file being diffed as 'fn'. If set
1479 called with the name of current file being diffed as 'fn'. If set
1484 to None, patches will always be upgraded to git format when
1480 to None, patches will always be upgraded to git format when
1485 necessary.
1481 necessary.
1486
1482
1487 prefix is a filename prefix that is prepended to all filenames on
1483 prefix is a filename prefix that is prepended to all filenames on
1488 display (used for subrepos).
1484 display (used for subrepos).
1489 '''
1485 '''
1490
1486
1491 if opts is None:
1487 if opts is None:
1492 opts = mdiff.defaultopts
1488 opts = mdiff.defaultopts
1493
1489
1494 if not node1 and not node2:
1490 if not node1 and not node2:
1495 node1 = repo.dirstate.p1()
1491 node1 = repo.dirstate.p1()
1496
1492
1497 def lrugetfilectx():
1493 def lrugetfilectx():
1498 cache = {}
1494 cache = {}
1499 order = []
1495 order = []
1500 def getfilectx(f, ctx):
1496 def getfilectx(f, ctx):
1501 fctx = ctx.filectx(f, filelog=cache.get(f))
1497 fctx = ctx.filectx(f, filelog=cache.get(f))
1502 if f not in cache:
1498 if f not in cache:
1503 if len(cache) > 20:
1499 if len(cache) > 20:
1504 del cache[order.pop(0)]
1500 del cache[order.pop(0)]
1505 cache[f] = fctx.filelog()
1501 cache[f] = fctx.filelog()
1506 else:
1502 else:
1507 order.remove(f)
1503 order.remove(f)
1508 order.append(f)
1504 order.append(f)
1509 return fctx
1505 return fctx
1510 return getfilectx
1506 return getfilectx
1511 getfilectx = lrugetfilectx()
1507 getfilectx = lrugetfilectx()
1512
1508
1513 ctx1 = repo[node1]
1509 ctx1 = repo[node1]
1514 ctx2 = repo[node2]
1510 ctx2 = repo[node2]
1515
1511
1516 if not changes:
1512 if not changes:
1517 changes = repo.status(ctx1, ctx2, match=match)
1513 changes = repo.status(ctx1, ctx2, match=match)
1518 modified, added, removed = changes[:3]
1514 modified, added, removed = changes[:3]
1519
1515
1520 if not modified and not added and not removed:
1516 if not modified and not added and not removed:
1521 return []
1517 return []
1522
1518
1523 revs = None
1519 revs = None
1524 if not repo.ui.quiet:
1520 if not repo.ui.quiet:
1525 hexfunc = repo.ui.debugflag and hex or short
1521 hexfunc = repo.ui.debugflag and hex or short
1526 revs = [hexfunc(node) for node in [node1, node2] if node]
1522 revs = [hexfunc(node) for node in [node1, node2] if node]
1527
1523
1528 copy = {}
1524 copy = {}
1529 if opts.git or opts.upgrade:
1525 if opts.git or opts.upgrade:
1530 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1526 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1531
1527
1532 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1528 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1533 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1529 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1534 if opts.upgrade and not opts.git:
1530 if opts.upgrade and not opts.git:
1535 try:
1531 try:
1536 def losedata(fn):
1532 def losedata(fn):
1537 if not losedatafn or not losedatafn(fn=fn):
1533 if not losedatafn or not losedatafn(fn=fn):
1538 raise GitDiffRequired()
1534 raise GitDiffRequired()
1539 # Buffer the whole output until we are sure it can be generated
1535 # Buffer the whole output until we are sure it can be generated
1540 return list(difffn(opts.copy(git=False), losedata))
1536 return list(difffn(opts.copy(git=False), losedata))
1541 except GitDiffRequired:
1537 except GitDiffRequired:
1542 return difffn(opts.copy(git=True), None)
1538 return difffn(opts.copy(git=True), None)
1543 else:
1539 else:
1544 return difffn(opts, None)
1540 return difffn(opts, None)
1545
1541
1546 def difflabel(func, *args, **kw):
1542 def difflabel(func, *args, **kw):
1547 '''yields 2-tuples of (output, label) based on the output of func()'''
1543 '''yields 2-tuples of (output, label) based on the output of func()'''
1548 prefixes = [('diff', 'diff.diffline'),
1544 prefixes = [('diff', 'diff.diffline'),
1549 ('copy', 'diff.extended'),
1545 ('copy', 'diff.extended'),
1550 ('rename', 'diff.extended'),
1546 ('rename', 'diff.extended'),
1551 ('old', 'diff.extended'),
1547 ('old', 'diff.extended'),
1552 ('new', 'diff.extended'),
1548 ('new', 'diff.extended'),
1553 ('deleted', 'diff.extended'),
1549 ('deleted', 'diff.extended'),
1554 ('---', 'diff.file_a'),
1550 ('---', 'diff.file_a'),
1555 ('+++', 'diff.file_b'),
1551 ('+++', 'diff.file_b'),
1556 ('@@', 'diff.hunk'),
1552 ('@@', 'diff.hunk'),
1557 ('-', 'diff.deleted'),
1553 ('-', 'diff.deleted'),
1558 ('+', 'diff.inserted')]
1554 ('+', 'diff.inserted')]
1559
1555
1560 for chunk in func(*args, **kw):
1556 for chunk in func(*args, **kw):
1561 lines = chunk.split('\n')
1557 lines = chunk.split('\n')
1562 for i, line in enumerate(lines):
1558 for i, line in enumerate(lines):
1563 if i != 0:
1559 if i != 0:
1564 yield ('\n', '')
1560 yield ('\n', '')
1565 stripline = line
1561 stripline = line
1566 if line and line[0] in '+-':
1562 if line and line[0] in '+-':
1567 # highlight trailing whitespace, but only in changed lines
1563 # highlight trailing whitespace, but only in changed lines
1568 stripline = line.rstrip()
1564 stripline = line.rstrip()
1569 for prefix, label in prefixes:
1565 for prefix, label in prefixes:
1570 if stripline.startswith(prefix):
1566 if stripline.startswith(prefix):
1571 yield (stripline, label)
1567 yield (stripline, label)
1572 break
1568 break
1573 else:
1569 else:
1574 yield (line, '')
1570 yield (line, '')
1575 if line != stripline:
1571 if line != stripline:
1576 yield (line[len(stripline):], 'diff.trailingwhitespace')
1572 yield (line[len(stripline):], 'diff.trailingwhitespace')
1577
1573
1578 def diffui(*args, **kw):
1574 def diffui(*args, **kw):
1579 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1575 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1580 return difflabel(diff, *args, **kw)
1576 return difflabel(diff, *args, **kw)
1581
1577
1582
1578
1583 def _addmodehdr(header, omode, nmode):
1579 def _addmodehdr(header, omode, nmode):
1584 if omode != nmode:
1580 if omode != nmode:
1585 header.append('old mode %s\n' % omode)
1581 header.append('old mode %s\n' % omode)
1586 header.append('new mode %s\n' % nmode)
1582 header.append('new mode %s\n' % nmode)
1587
1583
1588 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1584 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1589 copy, getfilectx, opts, losedatafn, prefix):
1585 copy, getfilectx, opts, losedatafn, prefix):
1590
1586
1591 def join(f):
1587 def join(f):
1592 return os.path.join(prefix, f)
1588 return os.path.join(prefix, f)
1593
1589
1594 date1 = util.datestr(ctx1.date())
1590 date1 = util.datestr(ctx1.date())
1595 man1 = ctx1.manifest()
1591 man1 = ctx1.manifest()
1596
1592
1597 gone = set()
1593 gone = set()
1598 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1594 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1599
1595
1600 copyto = dict([(v, k) for k, v in copy.items()])
1596 copyto = dict([(v, k) for k, v in copy.items()])
1601
1597
1602 if opts.git:
1598 if opts.git:
1603 revs = None
1599 revs = None
1604
1600
1605 for f in sorted(modified + added + removed):
1601 for f in sorted(modified + added + removed):
1606 to = None
1602 to = None
1607 tn = None
1603 tn = None
1608 dodiff = True
1604 dodiff = True
1609 header = []
1605 header = []
1610 if f in man1:
1606 if f in man1:
1611 to = getfilectx(f, ctx1).data()
1607 to = getfilectx(f, ctx1).data()
1612 if f not in removed:
1608 if f not in removed:
1613 tn = getfilectx(f, ctx2).data()
1609 tn = getfilectx(f, ctx2).data()
1614 a, b = f, f
1610 a, b = f, f
1615 if opts.git or losedatafn:
1611 if opts.git or losedatafn:
1616 if f in added:
1612 if f in added:
1617 mode = gitmode[ctx2.flags(f)]
1613 mode = gitmode[ctx2.flags(f)]
1618 if f in copy or f in copyto:
1614 if f in copy or f in copyto:
1619 if opts.git:
1615 if opts.git:
1620 if f in copy:
1616 if f in copy:
1621 a = copy[f]
1617 a = copy[f]
1622 else:
1618 else:
1623 a = copyto[f]
1619 a = copyto[f]
1624 omode = gitmode[man1.flags(a)]
1620 omode = gitmode[man1.flags(a)]
1625 _addmodehdr(header, omode, mode)
1621 _addmodehdr(header, omode, mode)
1626 if a in removed and a not in gone:
1622 if a in removed and a not in gone:
1627 op = 'rename'
1623 op = 'rename'
1628 gone.add(a)
1624 gone.add(a)
1629 else:
1625 else:
1630 op = 'copy'
1626 op = 'copy'
1631 header.append('%s from %s\n' % (op, join(a)))
1627 header.append('%s from %s\n' % (op, join(a)))
1632 header.append('%s to %s\n' % (op, join(f)))
1628 header.append('%s to %s\n' % (op, join(f)))
1633 to = getfilectx(a, ctx1).data()
1629 to = getfilectx(a, ctx1).data()
1634 else:
1630 else:
1635 losedatafn(f)
1631 losedatafn(f)
1636 else:
1632 else:
1637 if opts.git:
1633 if opts.git:
1638 header.append('new file mode %s\n' % mode)
1634 header.append('new file mode %s\n' % mode)
1639 elif ctx2.flags(f):
1635 elif ctx2.flags(f):
1640 losedatafn(f)
1636 losedatafn(f)
1641 # In theory, if tn was copied or renamed we should check
1637 # In theory, if tn was copied or renamed we should check
1642 # if the source is binary too but the copy record already
1638 # if the source is binary too but the copy record already
1643 # forces git mode.
1639 # forces git mode.
1644 if util.binary(tn):
1640 if util.binary(tn):
1645 if opts.git:
1641 if opts.git:
1646 dodiff = 'binary'
1642 dodiff = 'binary'
1647 else:
1643 else:
1648 losedatafn(f)
1644 losedatafn(f)
1649 if not opts.git and not tn:
1645 if not opts.git and not tn:
1650 # regular diffs cannot represent new empty file
1646 # regular diffs cannot represent new empty file
1651 losedatafn(f)
1647 losedatafn(f)
1652 elif f in removed:
1648 elif f in removed:
1653 if opts.git:
1649 if opts.git:
1654 # have we already reported a copy above?
1650 # have we already reported a copy above?
1655 if ((f in copy and copy[f] in added
1651 if ((f in copy and copy[f] in added
1656 and copyto[copy[f]] == f) or
1652 and copyto[copy[f]] == f) or
1657 (f in copyto and copyto[f] in added
1653 (f in copyto and copyto[f] in added
1658 and copy[copyto[f]] == f)):
1654 and copy[copyto[f]] == f)):
1659 dodiff = False
1655 dodiff = False
1660 else:
1656 else:
1661 header.append('deleted file mode %s\n' %
1657 header.append('deleted file mode %s\n' %
1662 gitmode[man1.flags(f)])
1658 gitmode[man1.flags(f)])
1663 elif not to or util.binary(to):
1659 elif not to or util.binary(to):
1664 # regular diffs cannot represent empty file deletion
1660 # regular diffs cannot represent empty file deletion
1665 losedatafn(f)
1661 losedatafn(f)
1666 else:
1662 else:
1667 oflag = man1.flags(f)
1663 oflag = man1.flags(f)
1668 nflag = ctx2.flags(f)
1664 nflag = ctx2.flags(f)
1669 binary = util.binary(to) or util.binary(tn)
1665 binary = util.binary(to) or util.binary(tn)
1670 if opts.git:
1666 if opts.git:
1671 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1667 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1672 if binary:
1668 if binary:
1673 dodiff = 'binary'
1669 dodiff = 'binary'
1674 elif binary or nflag != oflag:
1670 elif binary or nflag != oflag:
1675 losedatafn(f)
1671 losedatafn(f)
1676 if opts.git:
1672 if opts.git:
1677 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1673 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1678
1674
1679 if dodiff:
1675 if dodiff:
1680 if dodiff == 'binary':
1676 if dodiff == 'binary':
1681 text = b85diff(to, tn)
1677 text = b85diff(to, tn)
1682 else:
1678 else:
1683 text = mdiff.unidiff(to, date1,
1679 text = mdiff.unidiff(to, date1,
1684 # ctx2 date may be dynamic
1680 # ctx2 date may be dynamic
1685 tn, util.datestr(ctx2.date()),
1681 tn, util.datestr(ctx2.date()),
1686 join(a), join(b), revs, opts=opts)
1682 join(a), join(b), revs, opts=opts)
1687 if header and (text or len(header) > 1):
1683 if header and (text or len(header) > 1):
1688 yield ''.join(header)
1684 yield ''.join(header)
1689 if text:
1685 if text:
1690 yield text
1686 yield text
1691
1687
1692 def diffstatsum(stats):
1688 def diffstatsum(stats):
1693 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
1689 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
1694 for f, a, r, b in stats:
1690 for f, a, r, b in stats:
1695 maxfile = max(maxfile, encoding.colwidth(f))
1691 maxfile = max(maxfile, encoding.colwidth(f))
1696 maxtotal = max(maxtotal, a + r)
1692 maxtotal = max(maxtotal, a + r)
1697 addtotal += a
1693 addtotal += a
1698 removetotal += r
1694 removetotal += r
1699 binary = binary or b
1695 binary = binary or b
1700
1696
1701 return maxfile, maxtotal, addtotal, removetotal, binary
1697 return maxfile, maxtotal, addtotal, removetotal, binary
1702
1698
1703 def diffstatdata(lines):
1699 def diffstatdata(lines):
1704 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1700 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1705
1701
1706 results = []
1702 results = []
1707 filename, adds, removes = None, 0, 0
1703 filename, adds, removes = None, 0, 0
1708
1704
1709 def addresult():
1705 def addresult():
1710 if filename:
1706 if filename:
1711 isbinary = adds == 0 and removes == 0
1707 isbinary = adds == 0 and removes == 0
1712 results.append((filename, adds, removes, isbinary))
1708 results.append((filename, adds, removes, isbinary))
1713
1709
1714 for line in lines:
1710 for line in lines:
1715 if line.startswith('diff'):
1711 if line.startswith('diff'):
1716 addresult()
1712 addresult()
1717 # set numbers to 0 anyway when starting new file
1713 # set numbers to 0 anyway when starting new file
1718 adds, removes = 0, 0
1714 adds, removes = 0, 0
1719 if line.startswith('diff --git'):
1715 if line.startswith('diff --git'):
1720 filename = gitre.search(line).group(1)
1716 filename = gitre.search(line).group(1)
1721 elif line.startswith('diff -r'):
1717 elif line.startswith('diff -r'):
1722 # format: "diff -r ... -r ... filename"
1718 # format: "diff -r ... -r ... filename"
1723 filename = diffre.search(line).group(1)
1719 filename = diffre.search(line).group(1)
1724 elif line.startswith('+') and not line.startswith('+++'):
1720 elif line.startswith('+') and not line.startswith('+++'):
1725 adds += 1
1721 adds += 1
1726 elif line.startswith('-') and not line.startswith('---'):
1722 elif line.startswith('-') and not line.startswith('---'):
1727 removes += 1
1723 removes += 1
1728 addresult()
1724 addresult()
1729 return results
1725 return results
1730
1726
1731 def diffstat(lines, width=80, git=False):
1727 def diffstat(lines, width=80, git=False):
1732 output = []
1728 output = []
1733 stats = diffstatdata(lines)
1729 stats = diffstatdata(lines)
1734 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
1730 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
1735
1731
1736 countwidth = len(str(maxtotal))
1732 countwidth = len(str(maxtotal))
1737 if hasbinary and countwidth < 3:
1733 if hasbinary and countwidth < 3:
1738 countwidth = 3
1734 countwidth = 3
1739 graphwidth = width - countwidth - maxname - 6
1735 graphwidth = width - countwidth - maxname - 6
1740 if graphwidth < 10:
1736 if graphwidth < 10:
1741 graphwidth = 10
1737 graphwidth = 10
1742
1738
1743 def scale(i):
1739 def scale(i):
1744 if maxtotal <= graphwidth:
1740 if maxtotal <= graphwidth:
1745 return i
1741 return i
1746 # If diffstat runs out of room it doesn't print anything,
1742 # If diffstat runs out of room it doesn't print anything,
1747 # which isn't very useful, so always print at least one + or -
1743 # which isn't very useful, so always print at least one + or -
1748 # if there were at least some changes.
1744 # if there were at least some changes.
1749 return max(i * graphwidth // maxtotal, int(bool(i)))
1745 return max(i * graphwidth // maxtotal, int(bool(i)))
1750
1746
1751 for filename, adds, removes, isbinary in stats:
1747 for filename, adds, removes, isbinary in stats:
1752 if git and isbinary:
1748 if git and isbinary:
1753 count = 'Bin'
1749 count = 'Bin'
1754 else:
1750 else:
1755 count = adds + removes
1751 count = adds + removes
1756 pluses = '+' * scale(adds)
1752 pluses = '+' * scale(adds)
1757 minuses = '-' * scale(removes)
1753 minuses = '-' * scale(removes)
1758 output.append(' %s%s | %*s %s%s\n' %
1754 output.append(' %s%s | %*s %s%s\n' %
1759 (filename, ' ' * (maxname - encoding.colwidth(filename)),
1755 (filename, ' ' * (maxname - encoding.colwidth(filename)),
1760 countwidth, count, pluses, minuses))
1756 countwidth, count, pluses, minuses))
1761
1757
1762 if stats:
1758 if stats:
1763 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1759 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1764 % (len(stats), totaladds, totalremoves))
1760 % (len(stats), totaladds, totalremoves))
1765
1761
1766 return ''.join(output)
1762 return ''.join(output)
1767
1763
1768 def diffstatui(*args, **kw):
1764 def diffstatui(*args, **kw):
1769 '''like diffstat(), but yields 2-tuples of (output, label) for
1765 '''like diffstat(), but yields 2-tuples of (output, label) for
1770 ui.write()
1766 ui.write()
1771 '''
1767 '''
1772
1768
1773 for line in diffstat(*args, **kw).splitlines():
1769 for line in diffstat(*args, **kw).splitlines():
1774 if line and line[-1] in '+-':
1770 if line and line[-1] in '+-':
1775 name, graph = line.rsplit(' ', 1)
1771 name, graph = line.rsplit(' ', 1)
1776 yield (name + ' ', '')
1772 yield (name + ' ', '')
1777 m = re.search(r'\++', graph)
1773 m = re.search(r'\++', graph)
1778 if m:
1774 if m:
1779 yield (m.group(0), 'diffstat.inserted')
1775 yield (m.group(0), 'diffstat.inserted')
1780 m = re.search(r'-+', graph)
1776 m = re.search(r'-+', graph)
1781 if m:
1777 if m:
1782 yield (m.group(0), 'diffstat.deleted')
1778 yield (m.group(0), 'diffstat.deleted')
1783 else:
1779 else:
1784 yield (line, '')
1780 yield (line, '')
1785 yield ('\n', '')
1781 yield ('\n', '')
@@ -1,177 +1,177 b''
1 Setup extension:
1 Setup extension:
2
2
3 $ echo "[extensions]" >> $HGRCPATH
3 $ echo "[extensions]" >> $HGRCPATH
4 $ echo "mq =" >> $HGRCPATH
4 $ echo "mq =" >> $HGRCPATH
5 $ echo "[mq]" >> $HGRCPATH
5 $ echo "[mq]" >> $HGRCPATH
6 $ echo "git = keep" >> $HGRCPATH
6 $ echo "git = keep" >> $HGRCPATH
7
7
8 Test merge with mq changeset as the second parent:
8 Test merge with mq changeset as the second parent:
9
9
10 $ hg init m
10 $ hg init m
11 $ cd m
11 $ cd m
12 $ touch a b c
12 $ touch a b c
13 $ hg add a
13 $ hg add a
14 $ hg commit -m a
14 $ hg commit -m a
15 $ hg add b
15 $ hg add b
16 $ hg qnew -d "0 0" b
16 $ hg qnew -d "0 0" b
17 $ hg update 0
17 $ hg update 0
18 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
18 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
19 $ hg add c
19 $ hg add c
20 $ hg commit -m c
20 $ hg commit -m c
21 created new head
21 created new head
22 $ hg merge
22 $ hg merge
23 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
23 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
24 (branch merge, don't forget to commit)
24 (branch merge, don't forget to commit)
25 $ hg commit -m merge
25 $ hg commit -m merge
26 abort: cannot commit over an applied mq patch
26 abort: cannot commit over an applied mq patch
27 [255]
27 [255]
28 $ cd ..
28 $ cd ..
29
29
30 Issue529: mq aborts when merging patch deleting files
30 Issue529: mq aborts when merging patch deleting files
31
31
32 $ checkundo()
32 $ checkundo()
33 > {
33 > {
34 > if [ -f .hg/store/undo ]; then
34 > if [ -f .hg/store/undo ]; then
35 > echo ".hg/store/undo still exists"
35 > echo ".hg/store/undo still exists"
36 > fi
36 > fi
37 > }
37 > }
38
38
39 Commit two dummy files in "init" changeset:
39 Commit two dummy files in "init" changeset:
40
40
41 $ hg init t
41 $ hg init t
42 $ cd t
42 $ cd t
43 $ echo a > a
43 $ echo a > a
44 $ echo b > b
44 $ echo b > b
45 $ hg ci -Am init
45 $ hg ci -Am init
46 adding a
46 adding a
47 adding b
47 adding b
48 $ hg tag -l init
48 $ hg tag -l init
49
49
50 Create a patch removing a:
50 Create a patch removing a:
51
51
52 $ hg qnew rm_a
52 $ hg qnew rm_a
53 $ hg rm a
53 $ hg rm a
54 $ hg qrefresh -m "rm a"
54 $ hg qrefresh -m "rm a"
55
55
56 Save the patch queue so we can merge it later:
56 Save the patch queue so we can merge it later:
57
57
58 $ hg qsave -c -e
58 $ hg qsave -c -e
59 copy $TESTTMP/t/.hg/patches to $TESTTMP/t/.hg/patches.1
59 copy $TESTTMP/t/.hg/patches to $TESTTMP/t/.hg/patches.1
60 $ checkundo
60 $ checkundo
61
61
62 Update b and commit in an "update" changeset:
62 Update b and commit in an "update" changeset:
63
63
64 $ hg up -C init
64 $ hg up -C init
65 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
65 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
66 $ echo b >> b
66 $ echo b >> b
67 $ hg st
67 $ hg st
68 M b
68 M b
69 $ hg ci -m update
69 $ hg ci -m update
70 created new head
70 created new head
71
71
72 # Here, qpush used to abort with :
72 # Here, qpush used to abort with :
73 # The system cannot find the file specified => a
73 # The system cannot find the file specified => a
74 $ hg manifest
74 $ hg manifest
75 a
75 a
76 b
76 b
77
77
78 $ hg qpush -a -m
78 $ hg qpush -a -m
79 merging with queue at: $TESTTMP/t/.hg/patches.1
79 merging with queue at: $TESTTMP/t/.hg/patches.1
80 applying rm_a
80 applying rm_a
81 now at: rm_a
81 now at: rm_a
82
82
83 $ checkundo
83 $ checkundo
84 $ hg manifest
84 $ hg manifest
85 b
85 b
86
86
87 Ensure status is correct after merge:
87 Ensure status is correct after merge:
88
88
89 $ hg qpop -a
89 $ hg qpop -a
90 popping rm_a
90 popping rm_a
91 popping .hg.patches.merge.marker
91 popping .hg.patches.merge.marker
92 patch queue now empty
92 patch queue now empty
93
93
94 $ cd ..
94 $ cd ..
95
95
96 Classic MQ merge sequence *with an explicit named queue*:
96 Classic MQ merge sequence *with an explicit named queue*:
97
97
98 $ hg init t2
98 $ hg init t2
99 $ cd t2
99 $ cd t2
100 $ echo '[diff]' > .hg/hgrc
100 $ echo '[diff]' > .hg/hgrc
101 $ echo 'nodates = 1' >> .hg/hgrc
101 $ echo 'nodates = 1' >> .hg/hgrc
102 $ echo a > a
102 $ echo a > a
103 $ hg ci -Am init
103 $ hg ci -Am init
104 adding a
104 adding a
105 $ echo b > a
105 $ echo b > a
106 $ hg ci -m changea
106 $ hg ci -m changea
107 $ hg up -C 0
107 $ hg up -C 0
108 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
108 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
109 $ hg cp a aa
109 $ hg cp a aa
110 $ echo c >> a
110 $ echo c >> a
111 $ hg qnew --git -f -e patcha
111 $ hg qnew --git -f -e patcha
112 $ echo d >> a
112 $ echo d >> a
113 $ hg qnew -d '0 0' -f -e patcha2
113 $ hg qnew -d '0 0' -f -e patcha2
114
114
115 Create the reference queue:
115 Create the reference queue:
116
116
117 $ hg qsave -c -e -n refqueue
117 $ hg qsave -c -e -n refqueue
118 copy $TESTTMP/t2/.hg/patches to $TESTTMP/t2/.hg/refqueue
118 copy $TESTTMP/t2/.hg/patches to $TESTTMP/t2/.hg/refqueue
119 $ hg up -C 1
119 $ hg up -C 1
120 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
120 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
121
121
122 Merge:
122 Merge:
123
123
124 $ HGMERGE=internal:other hg qpush -a -m -n refqueue
124 $ HGMERGE=internal:other hg qpush -a -m -n refqueue
125 merging with queue at: $TESTTMP/t2/.hg/refqueue
125 merging with queue at: $TESTTMP/t2/.hg/refqueue
126 applying patcha
126 applying patcha
127 patching file a
127 patching file a
128 Hunk #1 FAILED at 0
128 Hunk #1 FAILED at 0
129 1 out of 1 hunks FAILED -- saving rejects to file a.rej
129 1 out of 1 hunks FAILED -- saving rejects to file a.rej
130 patch failed, unable to continue (try -v)
130 patch failed, unable to continue (try -v)
131 patch failed, rejects left in working dir
131 patch failed, rejects left in working dir
132 patch didn't work out, merging patcha
132 patch didn't work out, merging patcha
133 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
133 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
134 0 files updated, 2 files merged, 0 files removed, 0 files unresolved
134 0 files updated, 2 files merged, 0 files removed, 0 files unresolved
135 (branch merge, don't forget to commit)
135 (branch merge, don't forget to commit)
136 applying patcha2
136 applying patcha2
137 now at: patcha2
137 now at: patcha2
138
138
139 Check patcha is still a git patch:
139 Check patcha is still a git patch:
140
140
141 $ cat .hg/patches/patcha
141 $ cat .hg/patches/patcha
142 # HG changeset patch
142 # HG changeset patch
143 # Parent d3873e73d99ef67873dac33fbcc66268d5d2b6f4
143 # Parent d3873e73d99ef67873dac33fbcc66268d5d2b6f4
144
144
145 diff --git a/a b/a
145 diff --git a/a b/a
146 --- a/a
146 --- a/a
147 +++ b/a
147 +++ b/a
148 @@ -1,1 +1,2 @@
148 @@ -1,1 +1,2 @@
149 -b
149 -b
150 +a
150 +a
151 +c
151 +c
152 diff --git a/a b/aa
152 diff --git a/a b/aa
153 copy from a
153 copy from a
154 copy to aa
154 copy to aa
155 --- a/a
155 --- a/a
156 +++ b/aa
156 +++ b/aa
157 @@ -1,1 +1,1 @@
157 @@ -1,1 +1,1 @@
158 -b
158 -b
159 +a
159 +a
160
160
161 Check patcha2 is still a regular patch:
161 Check patcha2 is still a regular patch:
162
162
163 $ cat .hg/patches/patcha2
163 $ cat .hg/patches/patcha2
164 # HG changeset patch
164 # HG changeset patch
165 # Parent ???????????????????????????????????????? (glob)
165 # Parent ???????????????????????????????????????? (glob)
166 # Date 0 0
166 # Date 0 0
167
167
168 diff -r ???????????? -r ???????????? a (glob)
168 diff -r ???????????? -r ???????????? a (glob)
169 --- a/a
169 --- a/a
170 +++ b/a
170 +++ b/a
171 @@ -1,2 +1,3 @@
171 @@ -1,2 +1,3 @@
172 a
172 a
173 c
173 c
174 +d
174 +d
175
175
176 $ cd ..
176 $ cd ..
177
177
General Comments 0
You need to be logged in to leave comments. Login now