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