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