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