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