##// END OF EJS Templates
pytype: stop excluding patch.py...
Matt Harbison -
r49314:290f9c15 default
parent child Browse files
Show More
@@ -1,3268 +1,3270 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 from __future__ import absolute_import, print_function
10 10
11 11 import collections
12 12 import contextlib
13 13 import copy
14 14 import errno
15 15 import os
16 16 import re
17 17 import shutil
18 18 import zlib
19 19
20 20 from .i18n import _
21 21 from .node import (
22 22 hex,
23 23 sha1nodeconstants,
24 24 short,
25 25 )
26 26 from .pycompat import open
27 27 from . import (
28 28 copies,
29 29 diffhelper,
30 30 diffutil,
31 31 encoding,
32 32 error,
33 33 mail,
34 34 mdiff,
35 35 pathutil,
36 36 pycompat,
37 37 scmutil,
38 38 similar,
39 39 util,
40 40 vfs as vfsmod,
41 41 )
42 42 from .utils import (
43 43 dateutil,
44 44 hashutil,
45 45 procutil,
46 46 stringutil,
47 47 )
48 48
49 49 stringio = util.stringio
50 50
51 51 gitre = re.compile(br'diff --git a/(.*) b/(.*)')
52 52 tabsplitter = re.compile(br'(\t+|[^\t]+)')
53 53 wordsplitter = re.compile(
54 54 br'(\t+| +|[a-zA-Z0-9_\x80-\xff]+|[^ \ta-zA-Z0-9_\x80-\xff])'
55 55 )
56 56
57 57 PatchError = error.PatchError
58 58 PatchParseError = error.PatchParseError
59 59 PatchApplicationError = error.PatchApplicationError
60 60
61 61 # public functions
62 62
63 63
64 64 def split(stream):
65 65 '''return an iterator of individual patches from a stream'''
66 66
67 67 def isheader(line, inheader):
68 68 if inheader and line.startswith((b' ', b'\t')):
69 69 # continuation
70 70 return True
71 71 if line.startswith((b' ', b'-', b'+')):
72 72 # diff line - don't check for header pattern in there
73 73 return False
74 74 l = line.split(b': ', 1)
75 75 return len(l) == 2 and b' ' not in l[0]
76 76
77 77 def chunk(lines):
78 78 return stringio(b''.join(lines))
79 79
80 80 def hgsplit(stream, cur):
81 81 inheader = True
82 82
83 83 for line in stream:
84 84 if not line.strip():
85 85 inheader = False
86 86 if not inheader and line.startswith(b'# HG changeset patch'):
87 87 yield chunk(cur)
88 88 cur = []
89 89 inheader = True
90 90
91 91 cur.append(line)
92 92
93 93 if cur:
94 94 yield chunk(cur)
95 95
96 96 def mboxsplit(stream, cur):
97 97 for line in stream:
98 98 if line.startswith(b'From '):
99 99 for c in split(chunk(cur[1:])):
100 100 yield c
101 101 cur = []
102 102
103 103 cur.append(line)
104 104
105 105 if cur:
106 106 for c in split(chunk(cur[1:])):
107 107 yield c
108 108
109 109 def mimesplit(stream, cur):
110 110 def msgfp(m):
111 111 fp = stringio()
112 # pytype: disable=wrong-arg-types
112 113 g = mail.Generator(fp, mangle_from_=False)
114 # pytype: enable=wrong-arg-types
113 115 g.flatten(m)
114 116 fp.seek(0)
115 117 return fp
116 118
117 119 for line in stream:
118 120 cur.append(line)
119 121 c = chunk(cur)
120 122
121 123 m = mail.parse(c)
122 124 if not m.is_multipart():
123 125 yield msgfp(m)
124 126 else:
125 127 ok_types = (b'text/plain', b'text/x-diff', b'text/x-patch')
126 128 for part in m.walk():
127 129 ct = part.get_content_type()
128 130 if ct not in ok_types:
129 131 continue
130 132 yield msgfp(part)
131 133
132 134 def headersplit(stream, cur):
133 135 inheader = False
134 136
135 137 for line in stream:
136 138 if not inheader and isheader(line, inheader):
137 139 yield chunk(cur)
138 140 cur = []
139 141 inheader = True
140 142 if inheader and not isheader(line, inheader):
141 143 inheader = False
142 144
143 145 cur.append(line)
144 146
145 147 if cur:
146 148 yield chunk(cur)
147 149
148 150 def remainder(cur):
149 151 yield chunk(cur)
150 152
151 153 class fiter(object):
152 154 def __init__(self, fp):
153 155 self.fp = fp
154 156
155 157 def __iter__(self):
156 158 return self
157 159
158 160 def next(self):
159 161 l = self.fp.readline()
160 162 if not l:
161 163 raise StopIteration
162 164 return l
163 165
164 166 __next__ = next
165 167
166 168 inheader = False
167 169 cur = []
168 170
169 171 mimeheaders = [b'content-type']
170 172
171 173 if not util.safehasattr(stream, b'next'):
172 174 # http responses, for example, have readline but not next
173 175 stream = fiter(stream)
174 176
175 177 for line in stream:
176 178 cur.append(line)
177 179 if line.startswith(b'# HG changeset patch'):
178 180 return hgsplit(stream, cur)
179 181 elif line.startswith(b'From '):
180 182 return mboxsplit(stream, cur)
181 183 elif isheader(line, inheader):
182 184 inheader = True
183 185 if line.split(b':', 1)[0].lower() in mimeheaders:
184 186 # let email parser handle this
185 187 return mimesplit(stream, cur)
186 188 elif line.startswith(b'--- ') and inheader:
187 189 # No evil headers seen by diff start, split by hand
188 190 return headersplit(stream, cur)
189 191 # Not enough info, keep reading
190 192
191 193 # if we are here, we have a very plain patch
192 194 return remainder(cur)
193 195
194 196
195 197 ## Some facility for extensible patch parsing:
196 198 # list of pairs ("header to match", "data key")
197 199 patchheadermap = [
198 200 (b'Date', b'date'),
199 201 (b'Branch', b'branch'),
200 202 (b'Node ID', b'nodeid'),
201 203 ]
202 204
203 205
204 206 @contextlib.contextmanager
205 207 def extract(ui, fileobj):
206 208 """extract patch from data read from fileobj.
207 209
208 210 patch can be a normal patch or contained in an email message.
209 211
210 212 return a dictionary. Standard keys are:
211 213 - filename,
212 214 - message,
213 215 - user,
214 216 - date,
215 217 - branch,
216 218 - node,
217 219 - p1,
218 220 - p2.
219 221 Any item can be missing from the dictionary. If filename is missing,
220 222 fileobj did not contain a patch. Caller must unlink filename when done."""
221 223
222 224 fd, tmpname = pycompat.mkstemp(prefix=b'hg-patch-')
223 225 tmpfp = os.fdopen(fd, 'wb')
224 226 try:
225 227 yield _extract(ui, fileobj, tmpname, tmpfp)
226 228 finally:
227 229 tmpfp.close()
228 230 os.unlink(tmpname)
229 231
230 232
231 233 def _extract(ui, fileobj, tmpname, tmpfp):
232 234
233 235 # attempt to detect the start of a patch
234 236 # (this heuristic is borrowed from quilt)
235 237 diffre = re.compile(
236 238 br'^(?:Index:[ \t]|diff[ \t]-|RCS file: |'
237 239 br'retrieving revision [0-9]+(\.[0-9]+)*$|'
238 240 br'---[ \t].*?^\+\+\+[ \t]|'
239 241 br'\*\*\*[ \t].*?^---[ \t])',
240 242 re.MULTILINE | re.DOTALL,
241 243 )
242 244
243 245 data = {}
244 246
245 247 msg = mail.parse(fileobj)
246 248
247 249 subject = msg['Subject'] and mail.headdecode(msg['Subject'])
248 250 data[b'user'] = msg['From'] and mail.headdecode(msg['From'])
249 251 if not subject and not data[b'user']:
250 252 # Not an email, restore parsed headers if any
251 253 subject = (
252 254 b'\n'.join(
253 255 b': '.join(map(encoding.strtolocal, h)) for h in msg.items()
254 256 )
255 257 + b'\n'
256 258 )
257 259
258 260 # should try to parse msg['Date']
259 261 parents = []
260 262
261 263 nodeid = msg['X-Mercurial-Node']
262 264 if nodeid:
263 265 data[b'nodeid'] = nodeid = mail.headdecode(nodeid)
264 266 ui.debug(b'Node ID: %s\n' % nodeid)
265 267
266 268 if subject:
267 269 if subject.startswith(b'[PATCH'):
268 270 pend = subject.find(b']')
269 271 if pend >= 0:
270 272 subject = subject[pend + 1 :].lstrip()
271 273 subject = re.sub(br'\n[ \t]+', b' ', subject)
272 274 ui.debug(b'Subject: %s\n' % subject)
273 275 if data[b'user']:
274 276 ui.debug(b'From: %s\n' % data[b'user'])
275 277 diffs_seen = 0
276 278 ok_types = (b'text/plain', b'text/x-diff', b'text/x-patch')
277 279 message = b''
278 280 for part in msg.walk():
279 281 content_type = pycompat.bytestr(part.get_content_type())
280 282 ui.debug(b'Content-Type: %s\n' % content_type)
281 283 if content_type not in ok_types:
282 284 continue
283 285 payload = part.get_payload(decode=True)
284 286 m = diffre.search(payload)
285 287 if m:
286 288 hgpatch = False
287 289 hgpatchheader = False
288 290 ignoretext = False
289 291
290 292 ui.debug(b'found patch at byte %d\n' % m.start(0))
291 293 diffs_seen += 1
292 294 cfp = stringio()
293 295 for line in payload[: m.start(0)].splitlines():
294 296 if line.startswith(b'# HG changeset patch') and not hgpatch:
295 297 ui.debug(b'patch generated by hg export\n')
296 298 hgpatch = True
297 299 hgpatchheader = True
298 300 # drop earlier commit message content
299 301 cfp.seek(0)
300 302 cfp.truncate()
301 303 subject = None
302 304 elif hgpatchheader:
303 305 if line.startswith(b'# User '):
304 306 data[b'user'] = line[7:]
305 307 ui.debug(b'From: %s\n' % data[b'user'])
306 308 elif line.startswith(b"# Parent "):
307 309 parents.append(line[9:].lstrip())
308 310 elif line.startswith(b"# "):
309 311 for header, key in patchheadermap:
310 312 prefix = b'# %s ' % header
311 313 if line.startswith(prefix):
312 314 data[key] = line[len(prefix) :]
313 315 ui.debug(b'%s: %s\n' % (header, data[key]))
314 316 else:
315 317 hgpatchheader = False
316 318 elif line == b'---':
317 319 ignoretext = True
318 320 if not hgpatchheader and not ignoretext:
319 321 cfp.write(line)
320 322 cfp.write(b'\n')
321 323 message = cfp.getvalue()
322 324 if tmpfp:
323 325 tmpfp.write(payload)
324 326 if not payload.endswith(b'\n'):
325 327 tmpfp.write(b'\n')
326 328 elif not diffs_seen and message and content_type == b'text/plain':
327 329 message += b'\n' + payload
328 330
329 331 if subject and not message.startswith(subject):
330 332 message = b'%s\n%s' % (subject, message)
331 333 data[b'message'] = message
332 334 tmpfp.close()
333 335 if parents:
334 336 data[b'p1'] = parents.pop(0)
335 337 if parents:
336 338 data[b'p2'] = parents.pop(0)
337 339
338 340 if diffs_seen:
339 341 data[b'filename'] = tmpname
340 342
341 343 return data
342 344
343 345
344 346 class patchmeta(object):
345 347 """Patched file metadata
346 348
347 349 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
348 350 or COPY. 'path' is patched file path. 'oldpath' is set to the
349 351 origin file when 'op' is either COPY or RENAME, None otherwise. If
350 352 file mode is changed, 'mode' is a tuple (islink, isexec) where
351 353 'islink' is True if the file is a symlink and 'isexec' is True if
352 354 the file is executable. Otherwise, 'mode' is None.
353 355 """
354 356
355 357 def __init__(self, path):
356 358 self.path = path
357 359 self.oldpath = None
358 360 self.mode = None
359 361 self.op = b'MODIFY'
360 362 self.binary = False
361 363
362 364 def setmode(self, mode):
363 365 islink = mode & 0o20000
364 366 isexec = mode & 0o100
365 367 self.mode = (islink, isexec)
366 368
367 369 def copy(self):
368 370 other = patchmeta(self.path)
369 371 other.oldpath = self.oldpath
370 372 other.mode = self.mode
371 373 other.op = self.op
372 374 other.binary = self.binary
373 375 return other
374 376
375 377 def _ispatchinga(self, afile):
376 378 if afile == b'/dev/null':
377 379 return self.op == b'ADD'
378 380 return afile == b'a/' + (self.oldpath or self.path)
379 381
380 382 def _ispatchingb(self, bfile):
381 383 if bfile == b'/dev/null':
382 384 return self.op == b'DELETE'
383 385 return bfile == b'b/' + self.path
384 386
385 387 def ispatching(self, afile, bfile):
386 388 return self._ispatchinga(afile) and self._ispatchingb(bfile)
387 389
388 390 def __repr__(self):
389 391 return "<patchmeta %s %r>" % (self.op, self.path)
390 392
391 393
392 394 def readgitpatch(lr):
393 395 """extract git-style metadata about patches from <patchname>"""
394 396
395 397 # Filter patch for git information
396 398 gp = None
397 399 gitpatches = []
398 400 for line in lr:
399 401 line = line.rstrip(b'\r\n')
400 402 if line.startswith(b'diff --git a/'):
401 403 m = gitre.match(line)
402 404 if m:
403 405 if gp:
404 406 gitpatches.append(gp)
405 407 dst = m.group(2)
406 408 gp = patchmeta(dst)
407 409 elif gp:
408 410 if line.startswith(b'--- '):
409 411 gitpatches.append(gp)
410 412 gp = None
411 413 continue
412 414 if line.startswith(b'rename from '):
413 415 gp.op = b'RENAME'
414 416 gp.oldpath = line[12:]
415 417 elif line.startswith(b'rename to '):
416 418 gp.path = line[10:]
417 419 elif line.startswith(b'copy from '):
418 420 gp.op = b'COPY'
419 421 gp.oldpath = line[10:]
420 422 elif line.startswith(b'copy to '):
421 423 gp.path = line[8:]
422 424 elif line.startswith(b'deleted file'):
423 425 gp.op = b'DELETE'
424 426 elif line.startswith(b'new file mode '):
425 427 gp.op = b'ADD'
426 428 gp.setmode(int(line[-6:], 8))
427 429 elif line.startswith(b'new mode '):
428 430 gp.setmode(int(line[-6:], 8))
429 431 elif line.startswith(b'GIT binary patch'):
430 432 gp.binary = True
431 433 if gp:
432 434 gitpatches.append(gp)
433 435
434 436 return gitpatches
435 437
436 438
437 439 class linereader(object):
438 440 # simple class to allow pushing lines back into the input stream
439 441 def __init__(self, fp):
440 442 self.fp = fp
441 443 self.buf = []
442 444
443 445 def push(self, line):
444 446 if line is not None:
445 447 self.buf.append(line)
446 448
447 449 def readline(self):
448 450 if self.buf:
449 451 l = self.buf[0]
450 452 del self.buf[0]
451 453 return l
452 454 return self.fp.readline()
453 455
454 456 def __iter__(self):
455 457 return iter(self.readline, b'')
456 458
457 459
458 460 class abstractbackend(object):
459 461 def __init__(self, ui):
460 462 self.ui = ui
461 463
462 464 def getfile(self, fname):
463 465 """Return target file data and flags as a (data, (islink,
464 466 isexec)) tuple. Data is None if file is missing/deleted.
465 467 """
466 468 raise NotImplementedError
467 469
468 470 def setfile(self, fname, data, mode, copysource):
469 471 """Write data to target file fname and set its mode. mode is a
470 472 (islink, isexec) tuple. If data is None, the file content should
471 473 be left unchanged. If the file is modified after being copied,
472 474 copysource is set to the original file name.
473 475 """
474 476 raise NotImplementedError
475 477
476 478 def unlink(self, fname):
477 479 """Unlink target file."""
478 480 raise NotImplementedError
479 481
480 482 def writerej(self, fname, failed, total, lines):
481 483 """Write rejected lines for fname. total is the number of hunks
482 484 which failed to apply and total the total number of hunks for this
483 485 files.
484 486 """
485 487
486 488 def exists(self, fname):
487 489 raise NotImplementedError
488 490
489 491 def close(self):
490 492 raise NotImplementedError
491 493
492 494
493 495 class fsbackend(abstractbackend):
494 496 def __init__(self, ui, basedir):
495 497 super(fsbackend, self).__init__(ui)
496 498 self.opener = vfsmod.vfs(basedir)
497 499
498 500 def getfile(self, fname):
499 501 if self.opener.islink(fname):
500 502 return (self.opener.readlink(fname), (True, False))
501 503
502 504 isexec = False
503 505 try:
504 506 isexec = self.opener.lstat(fname).st_mode & 0o100 != 0
505 507 except OSError as e:
506 508 if e.errno != errno.ENOENT:
507 509 raise
508 510 try:
509 511 return (self.opener.read(fname), (False, isexec))
510 512 except IOError as e:
511 513 if e.errno != errno.ENOENT:
512 514 raise
513 515 return None, None
514 516
515 517 def setfile(self, fname, data, mode, copysource):
516 518 islink, isexec = mode
517 519 if data is None:
518 520 self.opener.setflags(fname, islink, isexec)
519 521 return
520 522 if islink:
521 523 self.opener.symlink(data, fname)
522 524 else:
523 525 self.opener.write(fname, data)
524 526 if isexec:
525 527 self.opener.setflags(fname, False, True)
526 528
527 529 def unlink(self, fname):
528 530 rmdir = self.ui.configbool(b'experimental', b'removeemptydirs')
529 531 self.opener.unlinkpath(fname, ignoremissing=True, rmdir=rmdir)
530 532
531 533 def writerej(self, fname, failed, total, lines):
532 534 fname = fname + b".rej"
533 535 self.ui.warn(
534 536 _(b"%d out of %d hunks FAILED -- saving rejects to file %s\n")
535 537 % (failed, total, fname)
536 538 )
537 539 fp = self.opener(fname, b'w')
538 540 fp.writelines(lines)
539 541 fp.close()
540 542
541 543 def exists(self, fname):
542 544 return self.opener.lexists(fname)
543 545
544 546
545 547 class workingbackend(fsbackend):
546 548 def __init__(self, ui, repo, similarity):
547 549 super(workingbackend, self).__init__(ui, repo.root)
548 550 self.repo = repo
549 551 self.similarity = similarity
550 552 self.removed = set()
551 553 self.changed = set()
552 554 self.copied = []
553 555
554 556 def _checkknown(self, fname):
555 557 if not self.repo.dirstate.get_entry(fname).any_tracked and self.exists(
556 558 fname
557 559 ):
558 560 raise PatchApplicationError(
559 561 _(b'cannot patch %s: file is not tracked') % fname
560 562 )
561 563
562 564 def setfile(self, fname, data, mode, copysource):
563 565 self._checkknown(fname)
564 566 super(workingbackend, self).setfile(fname, data, mode, copysource)
565 567 if copysource is not None:
566 568 self.copied.append((copysource, fname))
567 569 self.changed.add(fname)
568 570
569 571 def unlink(self, fname):
570 572 self._checkknown(fname)
571 573 super(workingbackend, self).unlink(fname)
572 574 self.removed.add(fname)
573 575 self.changed.add(fname)
574 576
575 577 def close(self):
576 578 wctx = self.repo[None]
577 579 changed = set(self.changed)
578 580 for src, dst in self.copied:
579 581 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
580 582 if self.removed:
581 583 wctx.forget(sorted(self.removed))
582 584 for f in self.removed:
583 585 if f not in self.repo.dirstate:
584 586 # File was deleted and no longer belongs to the
585 587 # dirstate, it was probably marked added then
586 588 # deleted, and should not be considered by
587 589 # marktouched().
588 590 changed.discard(f)
589 591 if changed:
590 592 scmutil.marktouched(self.repo, changed, self.similarity)
591 593 return sorted(self.changed)
592 594
593 595
594 596 class filestore(object):
595 597 def __init__(self, maxsize=None):
596 598 self.opener = None
597 599 self.files = {}
598 600 self.created = 0
599 601 self.maxsize = maxsize
600 602 if self.maxsize is None:
601 603 self.maxsize = 4 * (2 ** 20)
602 604 self.size = 0
603 605 self.data = {}
604 606
605 607 def setfile(self, fname, data, mode, copied=None):
606 608 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
607 609 self.data[fname] = (data, mode, copied)
608 610 self.size += len(data)
609 611 else:
610 612 if self.opener is None:
611 613 root = pycompat.mkdtemp(prefix=b'hg-patch-')
612 614 self.opener = vfsmod.vfs(root)
613 615 # Avoid filename issues with these simple names
614 616 fn = b'%d' % self.created
615 617 self.opener.write(fn, data)
616 618 self.created += 1
617 619 self.files[fname] = (fn, mode, copied)
618 620
619 621 def getfile(self, fname):
620 622 if fname in self.data:
621 623 return self.data[fname]
622 624 if not self.opener or fname not in self.files:
623 625 return None, None, None
624 626 fn, mode, copied = self.files[fname]
625 627 return self.opener.read(fn), mode, copied
626 628
627 629 def close(self):
628 630 if self.opener:
629 631 shutil.rmtree(self.opener.base)
630 632
631 633
632 634 class repobackend(abstractbackend):
633 635 def __init__(self, ui, repo, ctx, store):
634 636 super(repobackend, self).__init__(ui)
635 637 self.repo = repo
636 638 self.ctx = ctx
637 639 self.store = store
638 640 self.changed = set()
639 641 self.removed = set()
640 642 self.copied = {}
641 643
642 644 def _checkknown(self, fname):
643 645 if fname not in self.ctx:
644 646 raise PatchApplicationError(
645 647 _(b'cannot patch %s: file is not tracked') % fname
646 648 )
647 649
648 650 def getfile(self, fname):
649 651 try:
650 652 fctx = self.ctx[fname]
651 653 except error.LookupError:
652 654 return None, None
653 655 flags = fctx.flags()
654 656 return fctx.data(), (b'l' in flags, b'x' in flags)
655 657
656 658 def setfile(self, fname, data, mode, copysource):
657 659 if copysource:
658 660 self._checkknown(copysource)
659 661 if data is None:
660 662 data = self.ctx[fname].data()
661 663 self.store.setfile(fname, data, mode, copysource)
662 664 self.changed.add(fname)
663 665 if copysource:
664 666 self.copied[fname] = copysource
665 667
666 668 def unlink(self, fname):
667 669 self._checkknown(fname)
668 670 self.removed.add(fname)
669 671
670 672 def exists(self, fname):
671 673 return fname in self.ctx
672 674
673 675 def close(self):
674 676 return self.changed | self.removed
675 677
676 678
677 679 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
678 680 unidesc = re.compile(br'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
679 681 contextdesc = re.compile(br'(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
680 682 eolmodes = [b'strict', b'crlf', b'lf', b'auto']
681 683
682 684
683 685 class patchfile(object):
684 686 def __init__(self, ui, gp, backend, store, eolmode=b'strict'):
685 687 self.fname = gp.path
686 688 self.eolmode = eolmode
687 689 self.eol = None
688 690 self.backend = backend
689 691 self.ui = ui
690 692 self.lines = []
691 693 self.exists = False
692 694 self.missing = True
693 695 self.mode = gp.mode
694 696 self.copysource = gp.oldpath
695 697 self.create = gp.op in (b'ADD', b'COPY', b'RENAME')
696 698 self.remove = gp.op == b'DELETE'
697 699 if self.copysource is None:
698 700 data, mode = backend.getfile(self.fname)
699 701 else:
700 702 data, mode = store.getfile(self.copysource)[:2]
701 703 if data is not None:
702 704 self.exists = self.copysource is None or backend.exists(self.fname)
703 705 self.missing = False
704 706 if data:
705 707 self.lines = mdiff.splitnewlines(data)
706 708 if self.mode is None:
707 709 self.mode = mode
708 710 if self.lines:
709 711 # Normalize line endings
710 712 if self.lines[0].endswith(b'\r\n'):
711 713 self.eol = b'\r\n'
712 714 elif self.lines[0].endswith(b'\n'):
713 715 self.eol = b'\n'
714 716 if eolmode != b'strict':
715 717 nlines = []
716 718 for l in self.lines:
717 719 if l.endswith(b'\r\n'):
718 720 l = l[:-2] + b'\n'
719 721 nlines.append(l)
720 722 self.lines = nlines
721 723 else:
722 724 if self.create:
723 725 self.missing = False
724 726 if self.mode is None:
725 727 self.mode = (False, False)
726 728 if self.missing:
727 729 self.ui.warn(_(b"unable to find '%s' for patching\n") % self.fname)
728 730 self.ui.warn(
729 731 _(
730 732 b"(use '--prefix' to apply patch relative to the "
731 733 b"current directory)\n"
732 734 )
733 735 )
734 736
735 737 self.hash = {}
736 738 self.dirty = 0
737 739 self.offset = 0
738 740 self.skew = 0
739 741 self.rej = []
740 742 self.fileprinted = False
741 743 self.printfile(False)
742 744 self.hunks = 0
743 745
744 746 def writelines(self, fname, lines, mode):
745 747 if self.eolmode == b'auto':
746 748 eol = self.eol
747 749 elif self.eolmode == b'crlf':
748 750 eol = b'\r\n'
749 751 else:
750 752 eol = b'\n'
751 753
752 754 if self.eolmode != b'strict' and eol and eol != b'\n':
753 755 rawlines = []
754 756 for l in lines:
755 757 if l and l.endswith(b'\n'):
756 758 l = l[:-1] + eol
757 759 rawlines.append(l)
758 760 lines = rawlines
759 761
760 762 self.backend.setfile(fname, b''.join(lines), mode, self.copysource)
761 763
762 764 def printfile(self, warn):
763 765 if self.fileprinted:
764 766 return
765 767 if warn or self.ui.verbose:
766 768 self.fileprinted = True
767 769 s = _(b"patching file %s\n") % self.fname
768 770 if warn:
769 771 self.ui.warn(s)
770 772 else:
771 773 self.ui.note(s)
772 774
773 775 def findlines(self, l, linenum):
774 776 # looks through the hash and finds candidate lines. The
775 777 # result is a list of line numbers sorted based on distance
776 778 # from linenum
777 779
778 780 cand = self.hash.get(l, [])
779 781 if len(cand) > 1:
780 782 # resort our list of potentials forward then back.
781 783 cand.sort(key=lambda x: abs(x - linenum))
782 784 return cand
783 785
784 786 def write_rej(self):
785 787 # our rejects are a little different from patch(1). This always
786 788 # creates rejects in the same form as the original patch. A file
787 789 # header is inserted so that you can run the reject through patch again
788 790 # without having to type the filename.
789 791 if not self.rej:
790 792 return
791 793 base = os.path.basename(self.fname)
792 794 lines = [b"--- %s\n+++ %s\n" % (base, base)]
793 795 for x in self.rej:
794 796 for l in x.hunk:
795 797 lines.append(l)
796 798 if l[-1:] != b'\n':
797 799 lines.append(b'\n' + diffhelper.MISSING_NEWLINE_MARKER)
798 800 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
799 801
800 802 def apply(self, h):
801 803 if not h.complete():
802 804 raise PatchParseError(
803 805 _(b"bad hunk #%d %s (%d %d %d %d)")
804 806 % (h.number, h.desc, len(h.a), h.lena, len(h.b), h.lenb)
805 807 )
806 808
807 809 self.hunks += 1
808 810
809 811 if self.missing:
810 812 self.rej.append(h)
811 813 return -1
812 814
813 815 if self.exists and self.create:
814 816 if self.copysource:
815 817 self.ui.warn(
816 818 _(b"cannot create %s: destination already exists\n")
817 819 % self.fname
818 820 )
819 821 else:
820 822 self.ui.warn(_(b"file %s already exists\n") % self.fname)
821 823 self.rej.append(h)
822 824 return -1
823 825
824 826 if isinstance(h, binhunk):
825 827 if self.remove:
826 828 self.backend.unlink(self.fname)
827 829 else:
828 830 l = h.new(self.lines)
829 831 self.lines[:] = l
830 832 self.offset += len(l)
831 833 self.dirty = True
832 834 return 0
833 835
834 836 horig = h
835 837 if (
836 838 self.eolmode in (b'crlf', b'lf')
837 839 or self.eolmode == b'auto'
838 840 and self.eol
839 841 ):
840 842 # If new eols are going to be normalized, then normalize
841 843 # hunk data before patching. Otherwise, preserve input
842 844 # line-endings.
843 845 h = h.getnormalized()
844 846
845 847 # fast case first, no offsets, no fuzz
846 848 old, oldstart, new, newstart = h.fuzzit(0, False)
847 849 oldstart += self.offset
848 850 orig_start = oldstart
849 851 # if there's skew we want to emit the "(offset %d lines)" even
850 852 # when the hunk cleanly applies at start + skew, so skip the
851 853 # fast case code
852 854 if self.skew == 0 and diffhelper.testhunk(old, self.lines, oldstart):
853 855 if self.remove:
854 856 self.backend.unlink(self.fname)
855 857 else:
856 858 self.lines[oldstart : oldstart + len(old)] = new
857 859 self.offset += len(new) - len(old)
858 860 self.dirty = True
859 861 return 0
860 862
861 863 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
862 864 self.hash = {}
863 865 for x, s in enumerate(self.lines):
864 866 self.hash.setdefault(s, []).append(x)
865 867
866 868 for fuzzlen in pycompat.xrange(
867 869 self.ui.configint(b"patch", b"fuzz") + 1
868 870 ):
869 871 for toponly in [True, False]:
870 872 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
871 873 oldstart = oldstart + self.offset + self.skew
872 874 oldstart = min(oldstart, len(self.lines))
873 875 if old:
874 876 cand = self.findlines(old[0][1:], oldstart)
875 877 else:
876 878 # Only adding lines with no or fuzzed context, just
877 879 # take the skew in account
878 880 cand = [oldstart]
879 881
880 882 for l in cand:
881 883 if not old or diffhelper.testhunk(old, self.lines, l):
882 884 self.lines[l : l + len(old)] = new
883 885 self.offset += len(new) - len(old)
884 886 self.skew = l - orig_start
885 887 self.dirty = True
886 888 offset = l - orig_start - fuzzlen
887 889 if fuzzlen:
888 890 msg = _(
889 891 b"Hunk #%d succeeded at %d "
890 892 b"with fuzz %d "
891 893 b"(offset %d lines).\n"
892 894 )
893 895 self.printfile(True)
894 896 self.ui.warn(
895 897 msg % (h.number, l + 1, fuzzlen, offset)
896 898 )
897 899 else:
898 900 msg = _(
899 901 b"Hunk #%d succeeded at %d "
900 902 b"(offset %d lines).\n"
901 903 )
902 904 self.ui.note(msg % (h.number, l + 1, offset))
903 905 return fuzzlen
904 906 self.printfile(True)
905 907 self.ui.warn(_(b"Hunk #%d FAILED at %d\n") % (h.number, orig_start))
906 908 self.rej.append(horig)
907 909 return -1
908 910
909 911 def close(self):
910 912 if self.dirty:
911 913 self.writelines(self.fname, self.lines, self.mode)
912 914 self.write_rej()
913 915 return len(self.rej)
914 916
915 917
916 918 class header(object):
917 919 """patch header"""
918 920
919 921 diffgit_re = re.compile(b'diff --git a/(.*) b/(.*)$')
920 922 diff_re = re.compile(b'diff -r .* (.*)$')
921 923 allhunks_re = re.compile(b'(?:index|deleted file) ')
922 924 pretty_re = re.compile(b'(?:new file|deleted file) ')
923 925 special_re = re.compile(b'(?:index|deleted|copy|rename|new mode) ')
924 926 newfile_re = re.compile(b'(?:new file|copy to|rename to)')
925 927
926 928 def __init__(self, header):
927 929 self.header = header
928 930 self.hunks = []
929 931
930 932 def binary(self):
931 933 return any(h.startswith(b'index ') for h in self.header)
932 934
933 935 def pretty(self, fp):
934 936 for h in self.header:
935 937 if h.startswith(b'index '):
936 938 fp.write(_(b'this modifies a binary file (all or nothing)\n'))
937 939 break
938 940 if self.pretty_re.match(h):
939 941 fp.write(h)
940 942 if self.binary():
941 943 fp.write(_(b'this is a binary file\n'))
942 944 break
943 945 if h.startswith(b'---'):
944 946 fp.write(
945 947 _(b'%d hunks, %d lines changed\n')
946 948 % (
947 949 len(self.hunks),
948 950 sum([max(h.added, h.removed) for h in self.hunks]),
949 951 )
950 952 )
951 953 break
952 954 fp.write(h)
953 955
954 956 def write(self, fp):
955 957 fp.write(b''.join(self.header))
956 958
957 959 def allhunks(self):
958 960 return any(self.allhunks_re.match(h) for h in self.header)
959 961
960 962 def files(self):
961 963 match = self.diffgit_re.match(self.header[0])
962 964 if match:
963 965 fromfile, tofile = match.groups()
964 966 if fromfile == tofile:
965 967 return [fromfile]
966 968 return [fromfile, tofile]
967 969 else:
968 970 return self.diff_re.match(self.header[0]).groups()
969 971
970 972 def filename(self):
971 973 return self.files()[-1]
972 974
973 975 def __repr__(self):
974 976 return '<header %s>' % (
975 977 ' '.join(pycompat.rapply(pycompat.fsdecode, self.files()))
976 978 )
977 979
978 980 def isnewfile(self):
979 981 return any(self.newfile_re.match(h) for h in self.header)
980 982
981 983 def special(self):
982 984 # Special files are shown only at the header level and not at the hunk
983 985 # level for example a file that has been deleted is a special file.
984 986 # The user cannot change the content of the operation, in the case of
985 987 # the deleted file he has to take the deletion or not take it, he
986 988 # cannot take some of it.
987 989 # Newly added files are special if they are empty, they are not special
988 990 # if they have some content as we want to be able to change it
989 991 nocontent = len(self.header) == 2
990 992 emptynewfile = self.isnewfile() and nocontent
991 993 return emptynewfile or any(
992 994 self.special_re.match(h) for h in self.header
993 995 )
994 996
995 997
996 998 class recordhunk(object):
997 999 """patch hunk
998 1000
999 1001 XXX shouldn't we merge this with the other hunk class?
1000 1002 """
1001 1003
1002 1004 def __init__(
1003 1005 self,
1004 1006 header,
1005 1007 fromline,
1006 1008 toline,
1007 1009 proc,
1008 1010 before,
1009 1011 hunk,
1010 1012 after,
1011 1013 maxcontext=None,
1012 1014 ):
1013 1015 def trimcontext(lines, reverse=False):
1014 1016 if maxcontext is not None:
1015 1017 delta = len(lines) - maxcontext
1016 1018 if delta > 0:
1017 1019 if reverse:
1018 1020 return delta, lines[delta:]
1019 1021 else:
1020 1022 return delta, lines[:maxcontext]
1021 1023 return 0, lines
1022 1024
1023 1025 self.header = header
1024 1026 trimedbefore, self.before = trimcontext(before, True)
1025 1027 self.fromline = fromline + trimedbefore
1026 1028 self.toline = toline + trimedbefore
1027 1029 _trimedafter, self.after = trimcontext(after, False)
1028 1030 self.proc = proc
1029 1031 self.hunk = hunk
1030 1032 self.added, self.removed = self.countchanges(self.hunk)
1031 1033
1032 1034 def __eq__(self, v):
1033 1035 if not isinstance(v, recordhunk):
1034 1036 return False
1035 1037
1036 1038 return (
1037 1039 (v.hunk == self.hunk)
1038 1040 and (v.proc == self.proc)
1039 1041 and (self.fromline == v.fromline)
1040 1042 and (self.header.files() == v.header.files())
1041 1043 )
1042 1044
1043 1045 def __hash__(self):
1044 1046 return hash(
1045 1047 (
1046 1048 tuple(self.hunk),
1047 1049 tuple(self.header.files()),
1048 1050 self.fromline,
1049 1051 self.proc,
1050 1052 )
1051 1053 )
1052 1054
1053 1055 def countchanges(self, hunk):
1054 1056 """hunk -> (n+,n-)"""
1055 1057 add = len([h for h in hunk if h.startswith(b'+')])
1056 1058 rem = len([h for h in hunk if h.startswith(b'-')])
1057 1059 return add, rem
1058 1060
1059 1061 def reversehunk(self):
1060 1062 """return another recordhunk which is the reverse of the hunk
1061 1063
1062 1064 If this hunk is diff(A, B), the returned hunk is diff(B, A). To do
1063 1065 that, swap fromline/toline and +/- signs while keep other things
1064 1066 unchanged.
1065 1067 """
1066 1068 m = {b'+': b'-', b'-': b'+', b'\\': b'\\'}
1067 1069 hunk = [b'%s%s' % (m[l[0:1]], l[1:]) for l in self.hunk]
1068 1070 return recordhunk(
1069 1071 self.header,
1070 1072 self.toline,
1071 1073 self.fromline,
1072 1074 self.proc,
1073 1075 self.before,
1074 1076 hunk,
1075 1077 self.after,
1076 1078 )
1077 1079
1078 1080 def write(self, fp):
1079 1081 delta = len(self.before) + len(self.after)
1080 1082 if self.after and self.after[-1] == diffhelper.MISSING_NEWLINE_MARKER:
1081 1083 delta -= 1
1082 1084 fromlen = delta + self.removed
1083 1085 tolen = delta + self.added
1084 1086 fp.write(
1085 1087 b'@@ -%d,%d +%d,%d @@%s\n'
1086 1088 % (
1087 1089 self.fromline,
1088 1090 fromlen,
1089 1091 self.toline,
1090 1092 tolen,
1091 1093 self.proc and (b' ' + self.proc),
1092 1094 )
1093 1095 )
1094 1096 fp.write(b''.join(self.before + self.hunk + self.after))
1095 1097
1096 1098 pretty = write
1097 1099
1098 1100 def filename(self):
1099 1101 return self.header.filename()
1100 1102
1101 1103 @encoding.strmethod
1102 1104 def __repr__(self):
1103 1105 return b'<hunk %r@%d>' % (self.filename(), self.fromline)
1104 1106
1105 1107
1106 1108 def getmessages():
1107 1109 return {
1108 1110 b'multiple': {
1109 1111 b'apply': _(b"apply change %d/%d to '%s'?"),
1110 1112 b'discard': _(b"discard change %d/%d to '%s'?"),
1111 1113 b'keep': _(b"keep change %d/%d to '%s'?"),
1112 1114 b'record': _(b"record change %d/%d to '%s'?"),
1113 1115 },
1114 1116 b'single': {
1115 1117 b'apply': _(b"apply this change to '%s'?"),
1116 1118 b'discard': _(b"discard this change to '%s'?"),
1117 1119 b'keep': _(b"keep this change to '%s'?"),
1118 1120 b'record': _(b"record this change to '%s'?"),
1119 1121 },
1120 1122 b'help': {
1121 1123 b'apply': _(
1122 1124 b'[Ynesfdaq?]'
1123 1125 b'$$ &Yes, apply this change'
1124 1126 b'$$ &No, skip this change'
1125 1127 b'$$ &Edit this change manually'
1126 1128 b'$$ &Skip remaining changes to this file'
1127 1129 b'$$ Apply remaining changes to this &file'
1128 1130 b'$$ &Done, skip remaining changes and files'
1129 1131 b'$$ Apply &all changes to all remaining files'
1130 1132 b'$$ &Quit, applying no changes'
1131 1133 b'$$ &? (display help)'
1132 1134 ),
1133 1135 b'discard': _(
1134 1136 b'[Ynesfdaq?]'
1135 1137 b'$$ &Yes, discard this change'
1136 1138 b'$$ &No, skip this change'
1137 1139 b'$$ &Edit this change manually'
1138 1140 b'$$ &Skip remaining changes to this file'
1139 1141 b'$$ Discard remaining changes to this &file'
1140 1142 b'$$ &Done, skip remaining changes and files'
1141 1143 b'$$ Discard &all changes to all remaining files'
1142 1144 b'$$ &Quit, discarding no changes'
1143 1145 b'$$ &? (display help)'
1144 1146 ),
1145 1147 b'keep': _(
1146 1148 b'[Ynesfdaq?]'
1147 1149 b'$$ &Yes, keep this change'
1148 1150 b'$$ &No, skip this change'
1149 1151 b'$$ &Edit this change manually'
1150 1152 b'$$ &Skip remaining changes to this file'
1151 1153 b'$$ Keep remaining changes to this &file'
1152 1154 b'$$ &Done, skip remaining changes and files'
1153 1155 b'$$ Keep &all changes to all remaining files'
1154 1156 b'$$ &Quit, keeping all changes'
1155 1157 b'$$ &? (display help)'
1156 1158 ),
1157 1159 b'record': _(
1158 1160 b'[Ynesfdaq?]'
1159 1161 b'$$ &Yes, record this change'
1160 1162 b'$$ &No, skip this change'
1161 1163 b'$$ &Edit this change manually'
1162 1164 b'$$ &Skip remaining changes to this file'
1163 1165 b'$$ Record remaining changes to this &file'
1164 1166 b'$$ &Done, skip remaining changes and files'
1165 1167 b'$$ Record &all changes to all remaining files'
1166 1168 b'$$ &Quit, recording no changes'
1167 1169 b'$$ &? (display help)'
1168 1170 ),
1169 1171 },
1170 1172 }
1171 1173
1172 1174
1173 1175 def filterpatch(ui, headers, match, operation=None):
1174 1176 """Interactively filter patch chunks into applied-only chunks"""
1175 1177 messages = getmessages()
1176 1178
1177 1179 if operation is None:
1178 1180 operation = b'record'
1179 1181
1180 1182 def prompt(skipfile, skipall, query, chunk):
1181 1183 """prompt query, and process base inputs
1182 1184
1183 1185 - y/n for the rest of file
1184 1186 - y/n for the rest
1185 1187 - ? (help)
1186 1188 - q (quit)
1187 1189
1188 1190 Return True/False and possibly updated skipfile and skipall.
1189 1191 """
1190 1192 newpatches = None
1191 1193 if skipall is not None:
1192 1194 return skipall, skipfile, skipall, newpatches
1193 1195 if skipfile is not None:
1194 1196 return skipfile, skipfile, skipall, newpatches
1195 1197 while True:
1196 1198 resps = messages[b'help'][operation]
1197 1199 # IMPORTANT: keep the last line of this prompt short (<40 english
1198 1200 # chars is a good target) because of issue6158.
1199 1201 r = ui.promptchoice(b"%s\n(enter ? for help) %s" % (query, resps))
1200 1202 ui.write(b"\n")
1201 1203 if r == 8: # ?
1202 1204 for c, t in ui.extractchoices(resps)[1]:
1203 1205 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
1204 1206 continue
1205 1207 elif r == 0: # yes
1206 1208 ret = True
1207 1209 elif r == 1: # no
1208 1210 ret = False
1209 1211 elif r == 2: # Edit patch
1210 1212 if chunk is None:
1211 1213 ui.write(_(b'cannot edit patch for whole file'))
1212 1214 ui.write(b"\n")
1213 1215 continue
1214 1216 if chunk.header.binary():
1215 1217 ui.write(_(b'cannot edit patch for binary file'))
1216 1218 ui.write(b"\n")
1217 1219 continue
1218 1220 # Patch comment based on the Git one (based on comment at end of
1219 1221 # https://mercurial-scm.org/wiki/RecordExtension)
1220 1222 phelp = b'---' + _(
1221 1223 b"""
1222 1224 To remove '-' lines, make them ' ' lines (context).
1223 1225 To remove '+' lines, delete them.
1224 1226 Lines starting with # will be removed from the patch.
1225 1227
1226 1228 If the patch applies cleanly, the edited hunk will immediately be
1227 1229 added to the record list. If it does not apply cleanly, a rejects
1228 1230 file will be generated: you can use that when you try again. If
1229 1231 all lines of the hunk are removed, then the edit is aborted and
1230 1232 the hunk is left unchanged.
1231 1233 """
1232 1234 )
1233 1235 (patchfd, patchfn) = pycompat.mkstemp(
1234 1236 prefix=b"hg-editor-", suffix=b".diff"
1235 1237 )
1236 1238 ncpatchfp = None
1237 1239 try:
1238 1240 # Write the initial patch
1239 1241 f = util.nativeeolwriter(os.fdopen(patchfd, 'wb'))
1240 1242 chunk.header.write(f)
1241 1243 chunk.write(f)
1242 1244 f.write(
1243 1245 b''.join(
1244 1246 [b'# ' + i + b'\n' for i in phelp.splitlines()]
1245 1247 )
1246 1248 )
1247 1249 f.close()
1248 1250 # Start the editor and wait for it to complete
1249 1251 editor = ui.geteditor()
1250 1252 ret = ui.system(
1251 1253 b"%s \"%s\"" % (editor, patchfn),
1252 1254 environ={b'HGUSER': ui.username()},
1253 1255 blockedtag=b'filterpatch',
1254 1256 )
1255 1257 if ret != 0:
1256 1258 ui.warn(_(b"editor exited with exit code %d\n") % ret)
1257 1259 continue
1258 1260 # Remove comment lines
1259 1261 patchfp = open(patchfn, 'rb')
1260 1262 ncpatchfp = stringio()
1261 1263 for line in util.iterfile(patchfp):
1262 1264 line = util.fromnativeeol(line)
1263 1265 if not line.startswith(b'#'):
1264 1266 ncpatchfp.write(line)
1265 1267 patchfp.close()
1266 1268 ncpatchfp.seek(0)
1267 1269 newpatches = parsepatch(ncpatchfp)
1268 1270 finally:
1269 1271 os.unlink(patchfn)
1270 1272 del ncpatchfp
1271 1273 # Signal that the chunk shouldn't be applied as-is, but
1272 1274 # provide the new patch to be used instead.
1273 1275 ret = False
1274 1276 elif r == 3: # Skip
1275 1277 ret = skipfile = False
1276 1278 elif r == 4: # file (Record remaining)
1277 1279 ret = skipfile = True
1278 1280 elif r == 5: # done, skip remaining
1279 1281 ret = skipall = False
1280 1282 elif r == 6: # all
1281 1283 ret = skipall = True
1282 1284 elif r == 7: # quit
1283 1285 raise error.CanceledError(_(b'user quit'))
1284 1286 return ret, skipfile, skipall, newpatches
1285 1287
1286 1288 seen = set()
1287 1289 applied = {} # 'filename' -> [] of chunks
1288 1290 skipfile, skipall = None, None
1289 1291 pos, total = 1, sum(len(h.hunks) for h in headers)
1290 1292 for h in headers:
1291 1293 pos += len(h.hunks)
1292 1294 skipfile = None
1293 1295 fixoffset = 0
1294 1296 hdr = b''.join(h.header)
1295 1297 if hdr in seen:
1296 1298 continue
1297 1299 seen.add(hdr)
1298 1300 if skipall is None:
1299 1301 h.pretty(ui)
1300 1302 files = h.files()
1301 1303 msg = _(b'examine changes to %s?') % _(b' and ').join(
1302 1304 b"'%s'" % f for f in files
1303 1305 )
1304 1306 if all(match.exact(f) for f in files):
1305 1307 r, skipall, np = True, None, None
1306 1308 else:
1307 1309 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1308 1310 if not r:
1309 1311 continue
1310 1312 applied[h.filename()] = [h]
1311 1313 if h.allhunks():
1312 1314 applied[h.filename()] += h.hunks
1313 1315 continue
1314 1316 for i, chunk in enumerate(h.hunks):
1315 1317 if skipfile is None and skipall is None:
1316 1318 chunk.pretty(ui)
1317 1319 if total == 1:
1318 1320 msg = messages[b'single'][operation] % chunk.filename()
1319 1321 else:
1320 1322 idx = pos - len(h.hunks) + i
1321 1323 msg = messages[b'multiple'][operation] % (
1322 1324 idx,
1323 1325 total,
1324 1326 chunk.filename(),
1325 1327 )
1326 1328 r, skipfile, skipall, newpatches = prompt(
1327 1329 skipfile, skipall, msg, chunk
1328 1330 )
1329 1331 if r:
1330 1332 if fixoffset:
1331 1333 chunk = copy.copy(chunk)
1332 1334 chunk.toline += fixoffset
1333 1335 applied[chunk.filename()].append(chunk)
1334 1336 elif newpatches is not None:
1335 1337 for newpatch in newpatches:
1336 1338 for newhunk in newpatch.hunks:
1337 1339 if fixoffset:
1338 1340 newhunk.toline += fixoffset
1339 1341 applied[newhunk.filename()].append(newhunk)
1340 1342 else:
1341 1343 fixoffset += chunk.removed - chunk.added
1342 1344 return (
1343 1345 sum(
1344 1346 [
1345 1347 h
1346 1348 for h in pycompat.itervalues(applied)
1347 1349 if h[0].special() or len(h) > 1
1348 1350 ],
1349 1351 [],
1350 1352 ),
1351 1353 {},
1352 1354 )
1353 1355
1354 1356
1355 1357 class hunk(object):
1356 1358 def __init__(self, desc, num, lr, context):
1357 1359 self.number = num
1358 1360 self.desc = desc
1359 1361 self.hunk = [desc]
1360 1362 self.a = []
1361 1363 self.b = []
1362 1364 self.starta = self.lena = None
1363 1365 self.startb = self.lenb = None
1364 1366 if lr is not None:
1365 1367 if context:
1366 1368 self.read_context_hunk(lr)
1367 1369 else:
1368 1370 self.read_unified_hunk(lr)
1369 1371
1370 1372 def getnormalized(self):
1371 1373 """Return a copy with line endings normalized to LF."""
1372 1374
1373 1375 def normalize(lines):
1374 1376 nlines = []
1375 1377 for line in lines:
1376 1378 if line.endswith(b'\r\n'):
1377 1379 line = line[:-2] + b'\n'
1378 1380 nlines.append(line)
1379 1381 return nlines
1380 1382
1381 1383 # Dummy object, it is rebuilt manually
1382 1384 nh = hunk(self.desc, self.number, None, None)
1383 1385 nh.number = self.number
1384 1386 nh.desc = self.desc
1385 1387 nh.hunk = self.hunk
1386 1388 nh.a = normalize(self.a)
1387 1389 nh.b = normalize(self.b)
1388 1390 nh.starta = self.starta
1389 1391 nh.startb = self.startb
1390 1392 nh.lena = self.lena
1391 1393 nh.lenb = self.lenb
1392 1394 return nh
1393 1395
1394 1396 def read_unified_hunk(self, lr):
1395 1397 m = unidesc.match(self.desc)
1396 1398 if not m:
1397 1399 raise PatchParseError(_(b"bad hunk #%d") % self.number)
1398 1400 self.starta, self.lena, self.startb, self.lenb = m.groups()
1399 1401 if self.lena is None:
1400 1402 self.lena = 1
1401 1403 else:
1402 1404 self.lena = int(self.lena)
1403 1405 if self.lenb is None:
1404 1406 self.lenb = 1
1405 1407 else:
1406 1408 self.lenb = int(self.lenb)
1407 1409 self.starta = int(self.starta)
1408 1410 self.startb = int(self.startb)
1409 1411 try:
1410 1412 diffhelper.addlines(
1411 1413 lr, self.hunk, self.lena, self.lenb, self.a, self.b
1412 1414 )
1413 1415 except error.ParseError as e:
1414 1416 raise PatchParseError(_(b"bad hunk #%d: %s") % (self.number, e))
1415 1417 # if we hit eof before finishing out the hunk, the last line will
1416 1418 # be zero length. Lets try to fix it up.
1417 1419 while len(self.hunk[-1]) == 0:
1418 1420 del self.hunk[-1]
1419 1421 del self.a[-1]
1420 1422 del self.b[-1]
1421 1423 self.lena -= 1
1422 1424 self.lenb -= 1
1423 1425 self._fixnewline(lr)
1424 1426
1425 1427 def read_context_hunk(self, lr):
1426 1428 self.desc = lr.readline()
1427 1429 m = contextdesc.match(self.desc)
1428 1430 if not m:
1429 1431 raise PatchParseError(_(b"bad hunk #%d") % self.number)
1430 1432 self.starta, aend = m.groups()
1431 1433 self.starta = int(self.starta)
1432 1434 if aend is None:
1433 1435 aend = self.starta
1434 1436 self.lena = int(aend) - self.starta
1435 1437 if self.starta:
1436 1438 self.lena += 1
1437 1439 for x in pycompat.xrange(self.lena):
1438 1440 l = lr.readline()
1439 1441 if l.startswith(b'---'):
1440 1442 # lines addition, old block is empty
1441 1443 lr.push(l)
1442 1444 break
1443 1445 s = l[2:]
1444 1446 if l.startswith(b'- ') or l.startswith(b'! '):
1445 1447 u = b'-' + s
1446 1448 elif l.startswith(b' '):
1447 1449 u = b' ' + s
1448 1450 else:
1449 1451 raise PatchParseError(
1450 1452 _(b"bad hunk #%d old text line %d") % (self.number, x)
1451 1453 )
1452 1454 self.a.append(u)
1453 1455 self.hunk.append(u)
1454 1456
1455 1457 l = lr.readline()
1456 1458 if l.startswith(br'\ '):
1457 1459 s = self.a[-1][:-1]
1458 1460 self.a[-1] = s
1459 1461 self.hunk[-1] = s
1460 1462 l = lr.readline()
1461 1463 m = contextdesc.match(l)
1462 1464 if not m:
1463 1465 raise PatchParseError(_(b"bad hunk #%d") % self.number)
1464 1466 self.startb, bend = m.groups()
1465 1467 self.startb = int(self.startb)
1466 1468 if bend is None:
1467 1469 bend = self.startb
1468 1470 self.lenb = int(bend) - self.startb
1469 1471 if self.startb:
1470 1472 self.lenb += 1
1471 1473 hunki = 1
1472 1474 for x in pycompat.xrange(self.lenb):
1473 1475 l = lr.readline()
1474 1476 if l.startswith(br'\ '):
1475 1477 # XXX: the only way to hit this is with an invalid line range.
1476 1478 # The no-eol marker is not counted in the line range, but I
1477 1479 # guess there are diff(1) out there which behave differently.
1478 1480 s = self.b[-1][:-1]
1479 1481 self.b[-1] = s
1480 1482 self.hunk[hunki - 1] = s
1481 1483 continue
1482 1484 if not l:
1483 1485 # line deletions, new block is empty and we hit EOF
1484 1486 lr.push(l)
1485 1487 break
1486 1488 s = l[2:]
1487 1489 if l.startswith(b'+ ') or l.startswith(b'! '):
1488 1490 u = b'+' + s
1489 1491 elif l.startswith(b' '):
1490 1492 u = b' ' + s
1491 1493 elif len(self.b) == 0:
1492 1494 # line deletions, new block is empty
1493 1495 lr.push(l)
1494 1496 break
1495 1497 else:
1496 1498 raise PatchParseError(
1497 1499 _(b"bad hunk #%d old text line %d") % (self.number, x)
1498 1500 )
1499 1501 self.b.append(s)
1500 1502 while True:
1501 1503 if hunki >= len(self.hunk):
1502 1504 h = b""
1503 1505 else:
1504 1506 h = self.hunk[hunki]
1505 1507 hunki += 1
1506 1508 if h == u:
1507 1509 break
1508 1510 elif h.startswith(b'-'):
1509 1511 continue
1510 1512 else:
1511 1513 self.hunk.insert(hunki - 1, u)
1512 1514 break
1513 1515
1514 1516 if not self.a:
1515 1517 # this happens when lines were only added to the hunk
1516 1518 for x in self.hunk:
1517 1519 if x.startswith(b'-') or x.startswith(b' '):
1518 1520 self.a.append(x)
1519 1521 if not self.b:
1520 1522 # this happens when lines were only deleted from the hunk
1521 1523 for x in self.hunk:
1522 1524 if x.startswith(b'+') or x.startswith(b' '):
1523 1525 self.b.append(x[1:])
1524 1526 # @@ -start,len +start,len @@
1525 1527 self.desc = b"@@ -%d,%d +%d,%d @@\n" % (
1526 1528 self.starta,
1527 1529 self.lena,
1528 1530 self.startb,
1529 1531 self.lenb,
1530 1532 )
1531 1533 self.hunk[0] = self.desc
1532 1534 self._fixnewline(lr)
1533 1535
1534 1536 def _fixnewline(self, lr):
1535 1537 l = lr.readline()
1536 1538 if l.startswith(br'\ '):
1537 1539 diffhelper.fixnewline(self.hunk, self.a, self.b)
1538 1540 else:
1539 1541 lr.push(l)
1540 1542
1541 1543 def complete(self):
1542 1544 return len(self.a) == self.lena and len(self.b) == self.lenb
1543 1545
1544 1546 def _fuzzit(self, old, new, fuzz, toponly):
1545 1547 # this removes context lines from the top and bottom of list 'l'. It
1546 1548 # checks the hunk to make sure only context lines are removed, and then
1547 1549 # returns a new shortened list of lines.
1548 1550 fuzz = min(fuzz, len(old))
1549 1551 if fuzz:
1550 1552 top = 0
1551 1553 bot = 0
1552 1554 hlen = len(self.hunk)
1553 1555 for x in pycompat.xrange(hlen - 1):
1554 1556 # the hunk starts with the @@ line, so use x+1
1555 1557 if self.hunk[x + 1].startswith(b' '):
1556 1558 top += 1
1557 1559 else:
1558 1560 break
1559 1561 if not toponly:
1560 1562 for x in pycompat.xrange(hlen - 1):
1561 1563 if self.hunk[hlen - bot - 1].startswith(b' '):
1562 1564 bot += 1
1563 1565 else:
1564 1566 break
1565 1567
1566 1568 bot = min(fuzz, bot)
1567 1569 top = min(fuzz, top)
1568 1570 return old[top : len(old) - bot], new[top : len(new) - bot], top
1569 1571 return old, new, 0
1570 1572
1571 1573 def fuzzit(self, fuzz, toponly):
1572 1574 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1573 1575 oldstart = self.starta + top
1574 1576 newstart = self.startb + top
1575 1577 # zero length hunk ranges already have their start decremented
1576 1578 if self.lena and oldstart > 0:
1577 1579 oldstart -= 1
1578 1580 if self.lenb and newstart > 0:
1579 1581 newstart -= 1
1580 1582 return old, oldstart, new, newstart
1581 1583
1582 1584
1583 1585 class binhunk(object):
1584 1586 """A binary patch file."""
1585 1587
1586 1588 def __init__(self, lr, fname):
1587 1589 self.text = None
1588 1590 self.delta = False
1589 1591 self.hunk = [b'GIT binary patch\n']
1590 1592 self._fname = fname
1591 1593 self._read(lr)
1592 1594
1593 1595 def complete(self):
1594 1596 return self.text is not None
1595 1597
1596 1598 def new(self, lines):
1597 1599 if self.delta:
1598 1600 return [applybindelta(self.text, b''.join(lines))]
1599 1601 return [self.text]
1600 1602
1601 1603 def _read(self, lr):
1602 1604 def getline(lr, hunk):
1603 1605 l = lr.readline()
1604 1606 hunk.append(l)
1605 1607 return l.rstrip(b'\r\n')
1606 1608
1607 1609 while True:
1608 1610 line = getline(lr, self.hunk)
1609 1611 if not line:
1610 1612 raise PatchParseError(
1611 1613 _(b'could not extract "%s" binary data') % self._fname
1612 1614 )
1613 1615 if line.startswith(b'literal '):
1614 1616 size = int(line[8:].rstrip())
1615 1617 break
1616 1618 if line.startswith(b'delta '):
1617 1619 size = int(line[6:].rstrip())
1618 1620 self.delta = True
1619 1621 break
1620 1622 dec = []
1621 1623 line = getline(lr, self.hunk)
1622 1624 while len(line) > 1:
1623 1625 l = line[0:1]
1624 1626 if l <= b'Z' and l >= b'A':
1625 1627 l = ord(l) - ord(b'A') + 1
1626 1628 else:
1627 1629 l = ord(l) - ord(b'a') + 27
1628 1630 try:
1629 1631 dec.append(util.b85decode(line[1:])[:l])
1630 1632 except ValueError as e:
1631 1633 raise PatchParseError(
1632 1634 _(b'could not decode "%s" binary patch: %s')
1633 1635 % (self._fname, stringutil.forcebytestr(e))
1634 1636 )
1635 1637 line = getline(lr, self.hunk)
1636 1638 text = zlib.decompress(b''.join(dec))
1637 1639 if len(text) != size:
1638 1640 raise PatchParseError(
1639 1641 _(b'"%s" length is %d bytes, should be %d')
1640 1642 % (self._fname, len(text), size)
1641 1643 )
1642 1644 self.text = text
1643 1645
1644 1646
1645 1647 def parsefilename(str):
1646 1648 # --- filename \t|space stuff
1647 1649 s = str[4:].rstrip(b'\r\n')
1648 1650 i = s.find(b'\t')
1649 1651 if i < 0:
1650 1652 i = s.find(b' ')
1651 1653 if i < 0:
1652 1654 return s
1653 1655 return s[:i]
1654 1656
1655 1657
1656 1658 def reversehunks(hunks):
1657 1659 '''reverse the signs in the hunks given as argument
1658 1660
1659 1661 This function operates on hunks coming out of patch.filterpatch, that is
1660 1662 a list of the form: [header1, hunk1, hunk2, header2...]. Example usage:
1661 1663
1662 1664 >>> rawpatch = b"""diff --git a/folder1/g b/folder1/g
1663 1665 ... --- a/folder1/g
1664 1666 ... +++ b/folder1/g
1665 1667 ... @@ -1,7 +1,7 @@
1666 1668 ... +firstline
1667 1669 ... c
1668 1670 ... 1
1669 1671 ... 2
1670 1672 ... + 3
1671 1673 ... -4
1672 1674 ... 5
1673 1675 ... d
1674 1676 ... +lastline"""
1675 1677 >>> hunks = parsepatch([rawpatch])
1676 1678 >>> hunkscomingfromfilterpatch = []
1677 1679 >>> for h in hunks:
1678 1680 ... hunkscomingfromfilterpatch.append(h)
1679 1681 ... hunkscomingfromfilterpatch.extend(h.hunks)
1680 1682
1681 1683 >>> reversedhunks = reversehunks(hunkscomingfromfilterpatch)
1682 1684 >>> from . import util
1683 1685 >>> fp = util.stringio()
1684 1686 >>> for c in reversedhunks:
1685 1687 ... c.write(fp)
1686 1688 >>> fp.seek(0) or None
1687 1689 >>> reversedpatch = fp.read()
1688 1690 >>> print(pycompat.sysstr(reversedpatch))
1689 1691 diff --git a/folder1/g b/folder1/g
1690 1692 --- a/folder1/g
1691 1693 +++ b/folder1/g
1692 1694 @@ -1,4 +1,3 @@
1693 1695 -firstline
1694 1696 c
1695 1697 1
1696 1698 2
1697 1699 @@ -2,6 +1,6 @@
1698 1700 c
1699 1701 1
1700 1702 2
1701 1703 - 3
1702 1704 +4
1703 1705 5
1704 1706 d
1705 1707 @@ -6,3 +5,2 @@
1706 1708 5
1707 1709 d
1708 1710 -lastline
1709 1711
1710 1712 '''
1711 1713
1712 1714 newhunks = []
1713 1715 for c in hunks:
1714 1716 if util.safehasattr(c, b'reversehunk'):
1715 1717 c = c.reversehunk()
1716 1718 newhunks.append(c)
1717 1719 return newhunks
1718 1720
1719 1721
1720 1722 def parsepatch(originalchunks, maxcontext=None):
1721 1723 """patch -> [] of headers -> [] of hunks
1722 1724
1723 1725 If maxcontext is not None, trim context lines if necessary.
1724 1726
1725 1727 >>> rawpatch = b'''diff --git a/folder1/g b/folder1/g
1726 1728 ... --- a/folder1/g
1727 1729 ... +++ b/folder1/g
1728 1730 ... @@ -1,8 +1,10 @@
1729 1731 ... 1
1730 1732 ... 2
1731 1733 ... -3
1732 1734 ... 4
1733 1735 ... 5
1734 1736 ... 6
1735 1737 ... +6.1
1736 1738 ... +6.2
1737 1739 ... 7
1738 1740 ... 8
1739 1741 ... +9'''
1740 1742 >>> out = util.stringio()
1741 1743 >>> headers = parsepatch([rawpatch], maxcontext=1)
1742 1744 >>> for header in headers:
1743 1745 ... header.write(out)
1744 1746 ... for hunk in header.hunks:
1745 1747 ... hunk.write(out)
1746 1748 >>> print(pycompat.sysstr(out.getvalue()))
1747 1749 diff --git a/folder1/g b/folder1/g
1748 1750 --- a/folder1/g
1749 1751 +++ b/folder1/g
1750 1752 @@ -2,3 +2,2 @@
1751 1753 2
1752 1754 -3
1753 1755 4
1754 1756 @@ -6,2 +5,4 @@
1755 1757 6
1756 1758 +6.1
1757 1759 +6.2
1758 1760 7
1759 1761 @@ -8,1 +9,2 @@
1760 1762 8
1761 1763 +9
1762 1764 """
1763 1765
1764 1766 class parser(object):
1765 1767 """patch parsing state machine"""
1766 1768
1767 1769 def __init__(self):
1768 1770 self.fromline = 0
1769 1771 self.toline = 0
1770 1772 self.proc = b''
1771 1773 self.header = None
1772 1774 self.context = []
1773 1775 self.before = []
1774 1776 self.hunk = []
1775 1777 self.headers = []
1776 1778
1777 1779 def addrange(self, limits):
1778 1780 self.addcontext([])
1779 1781 fromstart, fromend, tostart, toend, proc = limits
1780 1782 self.fromline = int(fromstart)
1781 1783 self.toline = int(tostart)
1782 1784 self.proc = proc
1783 1785
1784 1786 def addcontext(self, context):
1785 1787 if self.hunk:
1786 1788 h = recordhunk(
1787 1789 self.header,
1788 1790 self.fromline,
1789 1791 self.toline,
1790 1792 self.proc,
1791 1793 self.before,
1792 1794 self.hunk,
1793 1795 context,
1794 1796 maxcontext,
1795 1797 )
1796 1798 self.header.hunks.append(h)
1797 1799 self.fromline += len(self.before) + h.removed
1798 1800 self.toline += len(self.before) + h.added
1799 1801 self.before = []
1800 1802 self.hunk = []
1801 1803 self.context = context
1802 1804
1803 1805 def addhunk(self, hunk):
1804 1806 if self.context:
1805 1807 self.before = self.context
1806 1808 self.context = []
1807 1809 if self.hunk:
1808 1810 self.addcontext([])
1809 1811 self.hunk = hunk
1810 1812
1811 1813 def newfile(self, hdr):
1812 1814 self.addcontext([])
1813 1815 h = header(hdr)
1814 1816 self.headers.append(h)
1815 1817 self.header = h
1816 1818
1817 1819 def addother(self, line):
1818 1820 pass # 'other' lines are ignored
1819 1821
1820 1822 def finished(self):
1821 1823 self.addcontext([])
1822 1824 return self.headers
1823 1825
1824 1826 transitions = {
1825 1827 b'file': {
1826 1828 b'context': addcontext,
1827 1829 b'file': newfile,
1828 1830 b'hunk': addhunk,
1829 1831 b'range': addrange,
1830 1832 },
1831 1833 b'context': {
1832 1834 b'file': newfile,
1833 1835 b'hunk': addhunk,
1834 1836 b'range': addrange,
1835 1837 b'other': addother,
1836 1838 },
1837 1839 b'hunk': {
1838 1840 b'context': addcontext,
1839 1841 b'file': newfile,
1840 1842 b'range': addrange,
1841 1843 },
1842 1844 b'range': {b'context': addcontext, b'hunk': addhunk},
1843 1845 b'other': {b'other': addother},
1844 1846 }
1845 1847
1846 1848 p = parser()
1847 1849 fp = stringio()
1848 1850 fp.write(b''.join(originalchunks))
1849 1851 fp.seek(0)
1850 1852
1851 1853 state = b'context'
1852 1854 for newstate, data in scanpatch(fp):
1853 1855 try:
1854 1856 p.transitions[state][newstate](p, data)
1855 1857 except KeyError:
1856 1858 raise PatchParseError(
1857 1859 b'unhandled transition: %s -> %s' % (state, newstate)
1858 1860 )
1859 1861 state = newstate
1860 1862 del fp
1861 1863 return p.finished()
1862 1864
1863 1865
1864 1866 def pathtransform(path, strip, prefix):
1865 1867 """turn a path from a patch into a path suitable for the repository
1866 1868
1867 1869 prefix, if not empty, is expected to be normalized with a / at the end.
1868 1870
1869 1871 Returns (stripped components, path in repository).
1870 1872
1871 1873 >>> pathtransform(b'a/b/c', 0, b'')
1872 1874 ('', 'a/b/c')
1873 1875 >>> pathtransform(b' a/b/c ', 0, b'')
1874 1876 ('', ' a/b/c')
1875 1877 >>> pathtransform(b' a/b/c ', 2, b'')
1876 1878 ('a/b/', 'c')
1877 1879 >>> pathtransform(b'a/b/c', 0, b'd/e/')
1878 1880 ('', 'd/e/a/b/c')
1879 1881 >>> pathtransform(b' a//b/c ', 2, b'd/e/')
1880 1882 ('a//b/', 'd/e/c')
1881 1883 >>> pathtransform(b'a/b/c', 3, b'')
1882 1884 Traceback (most recent call last):
1883 1885 PatchApplicationError: unable to strip away 1 of 3 dirs from a/b/c
1884 1886 """
1885 1887 pathlen = len(path)
1886 1888 i = 0
1887 1889 if strip == 0:
1888 1890 return b'', prefix + path.rstrip()
1889 1891 count = strip
1890 1892 while count > 0:
1891 1893 i = path.find(b'/', i)
1892 1894 if i == -1:
1893 1895 raise PatchApplicationError(
1894 1896 _(b"unable to strip away %d of %d dirs from %s")
1895 1897 % (count, strip, path)
1896 1898 )
1897 1899 i += 1
1898 1900 # consume '//' in the path
1899 1901 while i < pathlen - 1 and path[i : i + 1] == b'/':
1900 1902 i += 1
1901 1903 count -= 1
1902 1904 return path[:i].lstrip(), prefix + path[i:].rstrip()
1903 1905
1904 1906
1905 1907 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1906 1908 nulla = afile_orig == b"/dev/null"
1907 1909 nullb = bfile_orig == b"/dev/null"
1908 1910 create = nulla and hunk.starta == 0 and hunk.lena == 0
1909 1911 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1910 1912 abase, afile = pathtransform(afile_orig, strip, prefix)
1911 1913 gooda = not nulla and backend.exists(afile)
1912 1914 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1913 1915 if afile == bfile:
1914 1916 goodb = gooda
1915 1917 else:
1916 1918 goodb = not nullb and backend.exists(bfile)
1917 1919 missing = not goodb and not gooda and not create
1918 1920
1919 1921 # some diff programs apparently produce patches where the afile is
1920 1922 # not /dev/null, but afile starts with bfile
1921 1923 abasedir = afile[: afile.rfind(b'/') + 1]
1922 1924 bbasedir = bfile[: bfile.rfind(b'/') + 1]
1923 1925 if (
1924 1926 missing
1925 1927 and abasedir == bbasedir
1926 1928 and afile.startswith(bfile)
1927 1929 and hunk.starta == 0
1928 1930 and hunk.lena == 0
1929 1931 ):
1930 1932 create = True
1931 1933 missing = False
1932 1934
1933 1935 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1934 1936 # diff is between a file and its backup. In this case, the original
1935 1937 # file should be patched (see original mpatch code).
1936 1938 isbackup = abase == bbase and bfile.startswith(afile)
1937 1939 fname = None
1938 1940 if not missing:
1939 1941 if gooda and goodb:
1940 1942 if isbackup:
1941 1943 fname = afile
1942 1944 else:
1943 1945 fname = bfile
1944 1946 elif gooda:
1945 1947 fname = afile
1946 1948
1947 1949 if not fname:
1948 1950 if not nullb:
1949 1951 if isbackup:
1950 1952 fname = afile
1951 1953 else:
1952 1954 fname = bfile
1953 1955 elif not nulla:
1954 1956 fname = afile
1955 1957 else:
1956 1958 raise PatchParseError(_(b"undefined source and destination files"))
1957 1959
1958 1960 gp = patchmeta(fname)
1959 1961 if create:
1960 1962 gp.op = b'ADD'
1961 1963 elif remove:
1962 1964 gp.op = b'DELETE'
1963 1965 return gp
1964 1966
1965 1967
1966 1968 def scanpatch(fp):
1967 1969 """like patch.iterhunks, but yield different events
1968 1970
1969 1971 - ('file', [header_lines + fromfile + tofile])
1970 1972 - ('context', [context_lines])
1971 1973 - ('hunk', [hunk_lines])
1972 1974 - ('range', (-start,len, +start,len, proc))
1973 1975 """
1974 1976 lines_re = re.compile(br'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1975 1977 lr = linereader(fp)
1976 1978
1977 1979 def scanwhile(first, p):
1978 1980 """scan lr while predicate holds"""
1979 1981 lines = [first]
1980 1982 for line in iter(lr.readline, b''):
1981 1983 if p(line):
1982 1984 lines.append(line)
1983 1985 else:
1984 1986 lr.push(line)
1985 1987 break
1986 1988 return lines
1987 1989
1988 1990 for line in iter(lr.readline, b''):
1989 1991 if line.startswith(b'diff --git a/') or line.startswith(b'diff -r '):
1990 1992
1991 1993 def notheader(line):
1992 1994 s = line.split(None, 1)
1993 1995 return not s or s[0] not in (b'---', b'diff')
1994 1996
1995 1997 header = scanwhile(line, notheader)
1996 1998 fromfile = lr.readline()
1997 1999 if fromfile.startswith(b'---'):
1998 2000 tofile = lr.readline()
1999 2001 header += [fromfile, tofile]
2000 2002 else:
2001 2003 lr.push(fromfile)
2002 2004 yield b'file', header
2003 2005 elif line.startswith(b' '):
2004 2006 cs = (b' ', b'\\')
2005 2007 yield b'context', scanwhile(line, lambda l: l.startswith(cs))
2006 2008 elif line.startswith((b'-', b'+')):
2007 2009 cs = (b'-', b'+', b'\\')
2008 2010 yield b'hunk', scanwhile(line, lambda l: l.startswith(cs))
2009 2011 else:
2010 2012 m = lines_re.match(line)
2011 2013 if m:
2012 2014 yield b'range', m.groups()
2013 2015 else:
2014 2016 yield b'other', line
2015 2017
2016 2018
2017 2019 def scangitpatch(lr, firstline):
2018 2020 """
2019 2021 Git patches can emit:
2020 2022 - rename a to b
2021 2023 - change b
2022 2024 - copy a to c
2023 2025 - change c
2024 2026
2025 2027 We cannot apply this sequence as-is, the renamed 'a' could not be
2026 2028 found for it would have been renamed already. And we cannot copy
2027 2029 from 'b' instead because 'b' would have been changed already. So
2028 2030 we scan the git patch for copy and rename commands so we can
2029 2031 perform the copies ahead of time.
2030 2032 """
2031 2033 pos = 0
2032 2034 try:
2033 2035 pos = lr.fp.tell()
2034 2036 fp = lr.fp
2035 2037 except IOError:
2036 2038 fp = stringio(lr.fp.read())
2037 2039 gitlr = linereader(fp)
2038 2040 gitlr.push(firstline)
2039 2041 gitpatches = readgitpatch(gitlr)
2040 2042 fp.seek(pos)
2041 2043 return gitpatches
2042 2044
2043 2045
2044 2046 def iterhunks(fp):
2045 2047 """Read a patch and yield the following events:
2046 2048 - ("file", afile, bfile, firsthunk): select a new target file.
2047 2049 - ("hunk", hunk): a new hunk is ready to be applied, follows a
2048 2050 "file" event.
2049 2051 - ("git", gitchanges): current diff is in git format, gitchanges
2050 2052 maps filenames to gitpatch records. Unique event.
2051 2053 """
2052 2054 afile = b""
2053 2055 bfile = b""
2054 2056 state = None
2055 2057 hunknum = 0
2056 2058 emitfile = newfile = False
2057 2059 gitpatches = None
2058 2060
2059 2061 # our states
2060 2062 BFILE = 1
2061 2063 context = None
2062 2064 lr = linereader(fp)
2063 2065
2064 2066 for x in iter(lr.readline, b''):
2065 2067 if state == BFILE and (
2066 2068 (not context and x.startswith(b'@'))
2067 2069 or (context is not False and x.startswith(b'***************'))
2068 2070 or x.startswith(b'GIT binary patch')
2069 2071 ):
2070 2072 gp = None
2071 2073 if gitpatches and gitpatches[-1].ispatching(afile, bfile):
2072 2074 gp = gitpatches.pop()
2073 2075 if x.startswith(b'GIT binary patch'):
2074 2076 h = binhunk(lr, gp.path)
2075 2077 else:
2076 2078 if context is None and x.startswith(b'***************'):
2077 2079 context = True
2078 2080 h = hunk(x, hunknum + 1, lr, context)
2079 2081 hunknum += 1
2080 2082 if emitfile:
2081 2083 emitfile = False
2082 2084 yield b'file', (afile, bfile, h, gp and gp.copy() or None)
2083 2085 yield b'hunk', h
2084 2086 elif x.startswith(b'diff --git a/'):
2085 2087 m = gitre.match(x.rstrip(b'\r\n'))
2086 2088 if not m:
2087 2089 continue
2088 2090 if gitpatches is None:
2089 2091 # scan whole input for git metadata
2090 2092 gitpatches = scangitpatch(lr, x)
2091 2093 yield b'git', [
2092 2094 g.copy() for g in gitpatches if g.op in (b'COPY', b'RENAME')
2093 2095 ]
2094 2096 gitpatches.reverse()
2095 2097 afile = b'a/' + m.group(1)
2096 2098 bfile = b'b/' + m.group(2)
2097 2099 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
2098 2100 gp = gitpatches.pop()
2099 2101 yield b'file', (
2100 2102 b'a/' + gp.path,
2101 2103 b'b/' + gp.path,
2102 2104 None,
2103 2105 gp.copy(),
2104 2106 )
2105 2107 if not gitpatches:
2106 2108 raise PatchParseError(
2107 2109 _(b'failed to synchronize metadata for "%s"') % afile[2:]
2108 2110 )
2109 2111 newfile = True
2110 2112 elif x.startswith(b'---'):
2111 2113 # check for a unified diff
2112 2114 l2 = lr.readline()
2113 2115 if not l2.startswith(b'+++'):
2114 2116 lr.push(l2)
2115 2117 continue
2116 2118 newfile = True
2117 2119 context = False
2118 2120 afile = parsefilename(x)
2119 2121 bfile = parsefilename(l2)
2120 2122 elif x.startswith(b'***'):
2121 2123 # check for a context diff
2122 2124 l2 = lr.readline()
2123 2125 if not l2.startswith(b'---'):
2124 2126 lr.push(l2)
2125 2127 continue
2126 2128 l3 = lr.readline()
2127 2129 lr.push(l3)
2128 2130 if not l3.startswith(b"***************"):
2129 2131 lr.push(l2)
2130 2132 continue
2131 2133 newfile = True
2132 2134 context = True
2133 2135 afile = parsefilename(x)
2134 2136 bfile = parsefilename(l2)
2135 2137
2136 2138 if newfile:
2137 2139 newfile = False
2138 2140 emitfile = True
2139 2141 state = BFILE
2140 2142 hunknum = 0
2141 2143
2142 2144 while gitpatches:
2143 2145 gp = gitpatches.pop()
2144 2146 yield b'file', (b'a/' + gp.path, b'b/' + gp.path, None, gp.copy())
2145 2147
2146 2148
2147 2149 def applybindelta(binchunk, data):
2148 2150 """Apply a binary delta hunk
2149 2151 The algorithm used is the algorithm from git's patch-delta.c
2150 2152 """
2151 2153
2152 2154 def deltahead(binchunk):
2153 2155 i = 0
2154 2156 for c in pycompat.bytestr(binchunk):
2155 2157 i += 1
2156 2158 if not (ord(c) & 0x80):
2157 2159 return i
2158 2160 return i
2159 2161
2160 2162 out = b""
2161 2163 s = deltahead(binchunk)
2162 2164 binchunk = binchunk[s:]
2163 2165 s = deltahead(binchunk)
2164 2166 binchunk = binchunk[s:]
2165 2167 i = 0
2166 2168 while i < len(binchunk):
2167 2169 cmd = ord(binchunk[i : i + 1])
2168 2170 i += 1
2169 2171 if cmd & 0x80:
2170 2172 offset = 0
2171 2173 size = 0
2172 2174 if cmd & 0x01:
2173 2175 offset = ord(binchunk[i : i + 1])
2174 2176 i += 1
2175 2177 if cmd & 0x02:
2176 2178 offset |= ord(binchunk[i : i + 1]) << 8
2177 2179 i += 1
2178 2180 if cmd & 0x04:
2179 2181 offset |= ord(binchunk[i : i + 1]) << 16
2180 2182 i += 1
2181 2183 if cmd & 0x08:
2182 2184 offset |= ord(binchunk[i : i + 1]) << 24
2183 2185 i += 1
2184 2186 if cmd & 0x10:
2185 2187 size = ord(binchunk[i : i + 1])
2186 2188 i += 1
2187 2189 if cmd & 0x20:
2188 2190 size |= ord(binchunk[i : i + 1]) << 8
2189 2191 i += 1
2190 2192 if cmd & 0x40:
2191 2193 size |= ord(binchunk[i : i + 1]) << 16
2192 2194 i += 1
2193 2195 if size == 0:
2194 2196 size = 0x10000
2195 2197 offset_end = offset + size
2196 2198 out += data[offset:offset_end]
2197 2199 elif cmd != 0:
2198 2200 offset_end = i + cmd
2199 2201 out += binchunk[i:offset_end]
2200 2202 i += cmd
2201 2203 else:
2202 2204 raise PatchApplicationError(_(b'unexpected delta opcode 0'))
2203 2205 return out
2204 2206
2205 2207
2206 2208 def applydiff(ui, fp, backend, store, strip=1, prefix=b'', eolmode=b'strict'):
2207 2209 """Reads a patch from fp and tries to apply it.
2208 2210
2209 2211 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
2210 2212 there was any fuzz.
2211 2213
2212 2214 If 'eolmode' is 'strict', the patch content and patched file are
2213 2215 read in binary mode. Otherwise, line endings are ignored when
2214 2216 patching then normalized according to 'eolmode'.
2215 2217 """
2216 2218 return _applydiff(
2217 2219 ui,
2218 2220 fp,
2219 2221 patchfile,
2220 2222 backend,
2221 2223 store,
2222 2224 strip=strip,
2223 2225 prefix=prefix,
2224 2226 eolmode=eolmode,
2225 2227 )
2226 2228
2227 2229
2228 2230 def _canonprefix(repo, prefix):
2229 2231 if prefix:
2230 2232 prefix = pathutil.canonpath(repo.root, repo.getcwd(), prefix)
2231 2233 if prefix != b'':
2232 2234 prefix += b'/'
2233 2235 return prefix
2234 2236
2235 2237
2236 2238 def _applydiff(
2237 2239 ui, fp, patcher, backend, store, strip=1, prefix=b'', eolmode=b'strict'
2238 2240 ):
2239 2241 prefix = _canonprefix(backend.repo, prefix)
2240 2242
2241 2243 def pstrip(p):
2242 2244 return pathtransform(p, strip - 1, prefix)[1]
2243 2245
2244 2246 rejects = 0
2245 2247 err = 0
2246 2248 current_file = None
2247 2249
2248 2250 for state, values in iterhunks(fp):
2249 2251 if state == b'hunk':
2250 2252 if not current_file:
2251 2253 continue
2252 2254 ret = current_file.apply(values)
2253 2255 if ret > 0:
2254 2256 err = 1
2255 2257 elif state == b'file':
2256 2258 if current_file:
2257 2259 rejects += current_file.close()
2258 2260 current_file = None
2259 2261 afile, bfile, first_hunk, gp = values
2260 2262 if gp:
2261 2263 gp.path = pstrip(gp.path)
2262 2264 if gp.oldpath:
2263 2265 gp.oldpath = pstrip(gp.oldpath)
2264 2266 else:
2265 2267 gp = makepatchmeta(
2266 2268 backend, afile, bfile, first_hunk, strip, prefix
2267 2269 )
2268 2270 if gp.op == b'RENAME':
2269 2271 backend.unlink(gp.oldpath)
2270 2272 if not first_hunk:
2271 2273 if gp.op == b'DELETE':
2272 2274 backend.unlink(gp.path)
2273 2275 continue
2274 2276 data, mode = None, None
2275 2277 if gp.op in (b'RENAME', b'COPY'):
2276 2278 data, mode = store.getfile(gp.oldpath)[:2]
2277 2279 if data is None:
2278 2280 # This means that the old path does not exist
2279 2281 raise PatchApplicationError(
2280 2282 _(b"source file '%s' does not exist") % gp.oldpath
2281 2283 )
2282 2284 if gp.mode:
2283 2285 mode = gp.mode
2284 2286 if gp.op == b'ADD':
2285 2287 # Added files without content have no hunk and
2286 2288 # must be created
2287 2289 data = b''
2288 2290 if data or mode:
2289 2291 if gp.op in (b'ADD', b'RENAME', b'COPY') and backend.exists(
2290 2292 gp.path
2291 2293 ):
2292 2294 raise PatchApplicationError(
2293 2295 _(
2294 2296 b"cannot create %s: destination "
2295 2297 b"already exists"
2296 2298 )
2297 2299 % gp.path
2298 2300 )
2299 2301 backend.setfile(gp.path, data, mode, gp.oldpath)
2300 2302 continue
2301 2303 try:
2302 2304 current_file = patcher(ui, gp, backend, store, eolmode=eolmode)
2303 2305 except PatchError as inst:
2304 2306 ui.warn(stringutil.forcebytestr(inst) + b'\n')
2305 2307 current_file = None
2306 2308 rejects += 1
2307 2309 continue
2308 2310 elif state == b'git':
2309 2311 for gp in values:
2310 2312 path = pstrip(gp.oldpath)
2311 2313 data, mode = backend.getfile(path)
2312 2314 if data is None:
2313 2315 # The error ignored here will trigger a getfile()
2314 2316 # error in a place more appropriate for error
2315 2317 # handling, and will not interrupt the patching
2316 2318 # process.
2317 2319 pass
2318 2320 else:
2319 2321 store.setfile(path, data, mode)
2320 2322 else:
2321 2323 raise error.Abort(_(b'unsupported parser state: %s') % state)
2322 2324
2323 2325 if current_file:
2324 2326 rejects += current_file.close()
2325 2327
2326 2328 if rejects:
2327 2329 return -1
2328 2330 return err
2329 2331
2330 2332
2331 2333 def _externalpatch(ui, repo, patcher, patchname, strip, files, similarity):
2332 2334 """use <patcher> to apply <patchname> to the working directory.
2333 2335 returns whether patch was applied with fuzz factor."""
2334 2336
2335 2337 fuzz = False
2336 2338 args = []
2337 2339 cwd = repo.root
2338 2340 if cwd:
2339 2341 args.append(b'-d %s' % procutil.shellquote(cwd))
2340 2342 cmd = b'%s %s -p%d < %s' % (
2341 2343 patcher,
2342 2344 b' '.join(args),
2343 2345 strip,
2344 2346 procutil.shellquote(patchname),
2345 2347 )
2346 2348 ui.debug(b'Using external patch tool: %s\n' % cmd)
2347 2349 fp = procutil.popen(cmd, b'rb')
2348 2350 try:
2349 2351 for line in util.iterfile(fp):
2350 2352 line = line.rstrip()
2351 2353 ui.note(line + b'\n')
2352 2354 if line.startswith(b'patching file '):
2353 2355 pf = util.parsepatchoutput(line)
2354 2356 printed_file = False
2355 2357 files.add(pf)
2356 2358 elif line.find(b'with fuzz') >= 0:
2357 2359 fuzz = True
2358 2360 if not printed_file:
2359 2361 ui.warn(pf + b'\n')
2360 2362 printed_file = True
2361 2363 ui.warn(line + b'\n')
2362 2364 elif line.find(b'saving rejects to file') >= 0:
2363 2365 ui.warn(line + b'\n')
2364 2366 elif line.find(b'FAILED') >= 0:
2365 2367 if not printed_file:
2366 2368 ui.warn(pf + b'\n')
2367 2369 printed_file = True
2368 2370 ui.warn(line + b'\n')
2369 2371 finally:
2370 2372 if files:
2371 2373 scmutil.marktouched(repo, files, similarity)
2372 2374 code = fp.close()
2373 2375 if code:
2374 2376 raise PatchApplicationError(
2375 2377 _(b"patch command failed: %s") % procutil.explainexit(code)
2376 2378 )
2377 2379 return fuzz
2378 2380
2379 2381
2380 2382 def patchbackend(
2381 2383 ui, backend, patchobj, strip, prefix, files=None, eolmode=b'strict'
2382 2384 ):
2383 2385 if files is None:
2384 2386 files = set()
2385 2387 if eolmode is None:
2386 2388 eolmode = ui.config(b'patch', b'eol')
2387 2389 if eolmode.lower() not in eolmodes:
2388 2390 raise error.Abort(_(b'unsupported line endings type: %s') % eolmode)
2389 2391 eolmode = eolmode.lower()
2390 2392
2391 2393 store = filestore()
2392 2394 try:
2393 2395 fp = open(patchobj, b'rb')
2394 2396 except TypeError:
2395 2397 fp = patchobj
2396 2398 try:
2397 2399 ret = applydiff(
2398 2400 ui, fp, backend, store, strip=strip, prefix=prefix, eolmode=eolmode
2399 2401 )
2400 2402 finally:
2401 2403 if fp != patchobj:
2402 2404 fp.close()
2403 2405 files.update(backend.close())
2404 2406 store.close()
2405 2407 if ret < 0:
2406 2408 raise PatchApplicationError(_(b'patch failed to apply'))
2407 2409 return ret > 0
2408 2410
2409 2411
2410 2412 def internalpatch(
2411 2413 ui,
2412 2414 repo,
2413 2415 patchobj,
2414 2416 strip,
2415 2417 prefix=b'',
2416 2418 files=None,
2417 2419 eolmode=b'strict',
2418 2420 similarity=0,
2419 2421 ):
2420 2422 """use builtin patch to apply <patchobj> to the working directory.
2421 2423 returns whether patch was applied with fuzz factor."""
2422 2424 backend = workingbackend(ui, repo, similarity)
2423 2425 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2424 2426
2425 2427
2426 2428 def patchrepo(
2427 2429 ui, repo, ctx, store, patchobj, strip, prefix, files=None, eolmode=b'strict'
2428 2430 ):
2429 2431 backend = repobackend(ui, repo, ctx, store)
2430 2432 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2431 2433
2432 2434
2433 2435 def patch(
2434 2436 ui,
2435 2437 repo,
2436 2438 patchname,
2437 2439 strip=1,
2438 2440 prefix=b'',
2439 2441 files=None,
2440 2442 eolmode=b'strict',
2441 2443 similarity=0,
2442 2444 ):
2443 2445 """Apply <patchname> to the working directory.
2444 2446
2445 2447 'eolmode' specifies how end of lines should be handled. It can be:
2446 2448 - 'strict': inputs are read in binary mode, EOLs are preserved
2447 2449 - 'crlf': EOLs are ignored when patching and reset to CRLF
2448 2450 - 'lf': EOLs are ignored when patching and reset to LF
2449 2451 - None: get it from user settings, default to 'strict'
2450 2452 'eolmode' is ignored when using an external patcher program.
2451 2453
2452 2454 Returns whether patch was applied with fuzz factor.
2453 2455 """
2454 2456 patcher = ui.config(b'ui', b'patch')
2455 2457 if files is None:
2456 2458 files = set()
2457 2459 if patcher:
2458 2460 return _externalpatch(
2459 2461 ui, repo, patcher, patchname, strip, files, similarity
2460 2462 )
2461 2463 return internalpatch(
2462 2464 ui, repo, patchname, strip, prefix, files, eolmode, similarity
2463 2465 )
2464 2466
2465 2467
2466 2468 def changedfiles(ui, repo, patchpath, strip=1, prefix=b''):
2467 2469 backend = fsbackend(ui, repo.root)
2468 2470 prefix = _canonprefix(repo, prefix)
2469 2471 with open(patchpath, b'rb') as fp:
2470 2472 changed = set()
2471 2473 for state, values in iterhunks(fp):
2472 2474 if state == b'file':
2473 2475 afile, bfile, first_hunk, gp = values
2474 2476 if gp:
2475 2477 gp.path = pathtransform(gp.path, strip - 1, prefix)[1]
2476 2478 if gp.oldpath:
2477 2479 gp.oldpath = pathtransform(
2478 2480 gp.oldpath, strip - 1, prefix
2479 2481 )[1]
2480 2482 else:
2481 2483 gp = makepatchmeta(
2482 2484 backend, afile, bfile, first_hunk, strip, prefix
2483 2485 )
2484 2486 changed.add(gp.path)
2485 2487 if gp.op == b'RENAME':
2486 2488 changed.add(gp.oldpath)
2487 2489 elif state not in (b'hunk', b'git'):
2488 2490 raise error.Abort(_(b'unsupported parser state: %s') % state)
2489 2491 return changed
2490 2492
2491 2493
2492 2494 class GitDiffRequired(Exception):
2493 2495 pass
2494 2496
2495 2497
2496 2498 diffopts = diffutil.diffallopts
2497 2499 diffallopts = diffutil.diffallopts
2498 2500 difffeatureopts = diffutil.difffeatureopts
2499 2501
2500 2502
2501 2503 def diff(
2502 2504 repo,
2503 2505 node1=None,
2504 2506 node2=None,
2505 2507 match=None,
2506 2508 changes=None,
2507 2509 opts=None,
2508 2510 losedatafn=None,
2509 2511 pathfn=None,
2510 2512 copy=None,
2511 2513 copysourcematch=None,
2512 2514 hunksfilterfn=None,
2513 2515 ):
2514 2516 """yields diff of changes to files between two nodes, or node and
2515 2517 working directory.
2516 2518
2517 2519 if node1 is None, use first dirstate parent instead.
2518 2520 if node2 is None, compare node1 with working directory.
2519 2521
2520 2522 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2521 2523 every time some change cannot be represented with the current
2522 2524 patch format. Return False to upgrade to git patch format, True to
2523 2525 accept the loss or raise an exception to abort the diff. It is
2524 2526 called with the name of current file being diffed as 'fn'. If set
2525 2527 to None, patches will always be upgraded to git format when
2526 2528 necessary.
2527 2529
2528 2530 prefix is a filename prefix that is prepended to all filenames on
2529 2531 display (used for subrepos).
2530 2532
2531 2533 relroot, if not empty, must be normalized with a trailing /. Any match
2532 2534 patterns that fall outside it will be ignored.
2533 2535
2534 2536 copy, if not empty, should contain mappings {dst@y: src@x} of copy
2535 2537 information.
2536 2538
2537 2539 if copysourcematch is not None, then copy sources will be filtered by this
2538 2540 matcher
2539 2541
2540 2542 hunksfilterfn, if not None, should be a function taking a filectx and
2541 2543 hunks generator that may yield filtered hunks.
2542 2544 """
2543 2545 if not node1 and not node2:
2544 2546 node1 = repo.dirstate.p1()
2545 2547
2546 2548 ctx1 = repo[node1]
2547 2549 ctx2 = repo[node2]
2548 2550
2549 2551 for fctx1, fctx2, hdr, hunks in diffhunks(
2550 2552 repo,
2551 2553 ctx1=ctx1,
2552 2554 ctx2=ctx2,
2553 2555 match=match,
2554 2556 changes=changes,
2555 2557 opts=opts,
2556 2558 losedatafn=losedatafn,
2557 2559 pathfn=pathfn,
2558 2560 copy=copy,
2559 2561 copysourcematch=copysourcematch,
2560 2562 ):
2561 2563 if hunksfilterfn is not None:
2562 2564 # If the file has been removed, fctx2 is None; but this should
2563 2565 # not occur here since we catch removed files early in
2564 2566 # logcmdutil.getlinerangerevs() for 'hg log -L'.
2565 2567 assert (
2566 2568 fctx2 is not None
2567 2569 ), b'fctx2 unexpectly None in diff hunks filtering'
2568 2570 hunks = hunksfilterfn(fctx2, hunks)
2569 2571 text = b''.join(b''.join(hlines) for hrange, hlines in hunks)
2570 2572 if hdr and (text or len(hdr) > 1):
2571 2573 yield b'\n'.join(hdr) + b'\n'
2572 2574 if text:
2573 2575 yield text
2574 2576
2575 2577
2576 2578 def diffhunks(
2577 2579 repo,
2578 2580 ctx1,
2579 2581 ctx2,
2580 2582 match=None,
2581 2583 changes=None,
2582 2584 opts=None,
2583 2585 losedatafn=None,
2584 2586 pathfn=None,
2585 2587 copy=None,
2586 2588 copysourcematch=None,
2587 2589 ):
2588 2590 """Yield diff of changes to files in the form of (`header`, `hunks`) tuples
2589 2591 where `header` is a list of diff headers and `hunks` is an iterable of
2590 2592 (`hunkrange`, `hunklines`) tuples.
2591 2593
2592 2594 See diff() for the meaning of parameters.
2593 2595 """
2594 2596
2595 2597 if opts is None:
2596 2598 opts = mdiff.defaultopts
2597 2599
2598 2600 def lrugetfilectx():
2599 2601 cache = {}
2600 2602 order = collections.deque()
2601 2603
2602 2604 def getfilectx(f, ctx):
2603 2605 fctx = ctx.filectx(f, filelog=cache.get(f))
2604 2606 if f not in cache:
2605 2607 if len(cache) > 20:
2606 2608 del cache[order.popleft()]
2607 2609 cache[f] = fctx.filelog()
2608 2610 else:
2609 2611 order.remove(f)
2610 2612 order.append(f)
2611 2613 return fctx
2612 2614
2613 2615 return getfilectx
2614 2616
2615 2617 getfilectx = lrugetfilectx()
2616 2618
2617 2619 if not changes:
2618 2620 changes = ctx1.status(ctx2, match=match)
2619 2621 if isinstance(changes, list):
2620 2622 modified, added, removed = changes[:3]
2621 2623 else:
2622 2624 modified, added, removed = (
2623 2625 changes.modified,
2624 2626 changes.added,
2625 2627 changes.removed,
2626 2628 )
2627 2629
2628 2630 if not modified and not added and not removed:
2629 2631 return []
2630 2632
2631 2633 if repo.ui.debugflag:
2632 2634 hexfunc = hex
2633 2635 else:
2634 2636 hexfunc = short
2635 2637 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2636 2638
2637 2639 if copy is None:
2638 2640 copy = {}
2639 2641 if opts.git or opts.upgrade:
2640 2642 copy = copies.pathcopies(ctx1, ctx2, match=match)
2641 2643
2642 2644 if copysourcematch:
2643 2645 # filter out copies where source side isn't inside the matcher
2644 2646 # (copies.pathcopies() already filtered out the destination)
2645 2647 copy = {
2646 2648 dst: src
2647 2649 for dst, src in pycompat.iteritems(copy)
2648 2650 if copysourcematch(src)
2649 2651 }
2650 2652
2651 2653 modifiedset = set(modified)
2652 2654 addedset = set(added)
2653 2655 removedset = set(removed)
2654 2656 for f in modified:
2655 2657 if f not in ctx1:
2656 2658 # Fix up added, since merged-in additions appear as
2657 2659 # modifications during merges
2658 2660 modifiedset.remove(f)
2659 2661 addedset.add(f)
2660 2662 for f in removed:
2661 2663 if f not in ctx1:
2662 2664 # Merged-in additions that are then removed are reported as removed.
2663 2665 # They are not in ctx1, so We don't want to show them in the diff.
2664 2666 removedset.remove(f)
2665 2667 modified = sorted(modifiedset)
2666 2668 added = sorted(addedset)
2667 2669 removed = sorted(removedset)
2668 2670 for dst, src in list(copy.items()):
2669 2671 if src not in ctx1:
2670 2672 # Files merged in during a merge and then copied/renamed are
2671 2673 # reported as copies. We want to show them in the diff as additions.
2672 2674 del copy[dst]
2673 2675
2674 2676 prefetchmatch = scmutil.matchfiles(
2675 2677 repo, list(modifiedset | addedset | removedset)
2676 2678 )
2677 2679 revmatches = [
2678 2680 (ctx1.rev(), prefetchmatch),
2679 2681 (ctx2.rev(), prefetchmatch),
2680 2682 ]
2681 2683 scmutil.prefetchfiles(repo, revmatches)
2682 2684
2683 2685 def difffn(opts, losedata):
2684 2686 return trydiff(
2685 2687 repo,
2686 2688 revs,
2687 2689 ctx1,
2688 2690 ctx2,
2689 2691 modified,
2690 2692 added,
2691 2693 removed,
2692 2694 copy,
2693 2695 getfilectx,
2694 2696 opts,
2695 2697 losedata,
2696 2698 pathfn,
2697 2699 )
2698 2700
2699 2701 if opts.upgrade and not opts.git:
2700 2702 try:
2701 2703
2702 2704 def losedata(fn):
2703 2705 if not losedatafn or not losedatafn(fn=fn):
2704 2706 raise GitDiffRequired
2705 2707
2706 2708 # Buffer the whole output until we are sure it can be generated
2707 2709 return list(difffn(opts.copy(git=False), losedata))
2708 2710 except GitDiffRequired:
2709 2711 return difffn(opts.copy(git=True), None)
2710 2712 else:
2711 2713 return difffn(opts, None)
2712 2714
2713 2715
2714 2716 def diffsinglehunk(hunklines):
2715 2717 """yield tokens for a list of lines in a single hunk"""
2716 2718 for line in hunklines:
2717 2719 # chomp
2718 2720 chompline = line.rstrip(b'\r\n')
2719 2721 # highlight tabs and trailing whitespace
2720 2722 stripline = chompline.rstrip()
2721 2723 if line.startswith(b'-'):
2722 2724 label = b'diff.deleted'
2723 2725 elif line.startswith(b'+'):
2724 2726 label = b'diff.inserted'
2725 2727 else:
2726 2728 raise error.ProgrammingError(b'unexpected hunk line: %s' % line)
2727 2729 for token in tabsplitter.findall(stripline):
2728 2730 if token.startswith(b'\t'):
2729 2731 yield (token, b'diff.tab')
2730 2732 else:
2731 2733 yield (token, label)
2732 2734
2733 2735 if chompline != stripline:
2734 2736 yield (chompline[len(stripline) :], b'diff.trailingwhitespace')
2735 2737 if chompline != line:
2736 2738 yield (line[len(chompline) :], b'')
2737 2739
2738 2740
2739 2741 def diffsinglehunkinline(hunklines):
2740 2742 """yield tokens for a list of lines in a single hunk, with inline colors"""
2741 2743 # prepare deleted, and inserted content
2742 2744 a = bytearray()
2743 2745 b = bytearray()
2744 2746 for line in hunklines:
2745 2747 if line[0:1] == b'-':
2746 2748 a += line[1:]
2747 2749 elif line[0:1] == b'+':
2748 2750 b += line[1:]
2749 2751 else:
2750 2752 raise error.ProgrammingError(b'unexpected hunk line: %s' % line)
2751 2753 # fast path: if either side is empty, use diffsinglehunk
2752 2754 if not a or not b:
2753 2755 for t in diffsinglehunk(hunklines):
2754 2756 yield t
2755 2757 return
2756 2758 # re-split the content into words
2757 2759 al = wordsplitter.findall(bytes(a))
2758 2760 bl = wordsplitter.findall(bytes(b))
2759 2761 # re-arrange the words to lines since the diff algorithm is line-based
2760 2762 aln = [s if s == b'\n' else s + b'\n' for s in al]
2761 2763 bln = [s if s == b'\n' else s + b'\n' for s in bl]
2762 2764 an = b''.join(aln)
2763 2765 bn = b''.join(bln)
2764 2766 # run the diff algorithm, prepare atokens and btokens
2765 2767 atokens = []
2766 2768 btokens = []
2767 2769 blocks = mdiff.allblocks(an, bn, lines1=aln, lines2=bln)
2768 2770 for (a1, a2, b1, b2), btype in blocks:
2769 2771 changed = btype == b'!'
2770 2772 for token in mdiff.splitnewlines(b''.join(al[a1:a2])):
2771 2773 atokens.append((changed, token))
2772 2774 for token in mdiff.splitnewlines(b''.join(bl[b1:b2])):
2773 2775 btokens.append((changed, token))
2774 2776
2775 2777 # yield deleted tokens, then inserted ones
2776 2778 for prefix, label, tokens in [
2777 2779 (b'-', b'diff.deleted', atokens),
2778 2780 (b'+', b'diff.inserted', btokens),
2779 2781 ]:
2780 2782 nextisnewline = True
2781 2783 for changed, token in tokens:
2782 2784 if nextisnewline:
2783 2785 yield (prefix, label)
2784 2786 nextisnewline = False
2785 2787 # special handling line end
2786 2788 isendofline = token.endswith(b'\n')
2787 2789 if isendofline:
2788 2790 chomp = token[:-1] # chomp
2789 2791 if chomp.endswith(b'\r'):
2790 2792 chomp = chomp[:-1]
2791 2793 endofline = token[len(chomp) :]
2792 2794 token = chomp.rstrip() # detect spaces at the end
2793 2795 endspaces = chomp[len(token) :]
2794 2796 # scan tabs
2795 2797 for maybetab in tabsplitter.findall(token):
2796 2798 if b'\t' == maybetab[0:1]:
2797 2799 currentlabel = b'diff.tab'
2798 2800 else:
2799 2801 if changed:
2800 2802 currentlabel = label + b'.changed'
2801 2803 else:
2802 2804 currentlabel = label + b'.unchanged'
2803 2805 yield (maybetab, currentlabel)
2804 2806 if isendofline:
2805 2807 if endspaces:
2806 2808 yield (endspaces, b'diff.trailingwhitespace')
2807 2809 yield (endofline, b'')
2808 2810 nextisnewline = True
2809 2811
2810 2812
2811 2813 def difflabel(func, *args, **kw):
2812 2814 '''yields 2-tuples of (output, label) based on the output of func()'''
2813 2815 if kw.get('opts') and kw['opts'].worddiff:
2814 2816 dodiffhunk = diffsinglehunkinline
2815 2817 else:
2816 2818 dodiffhunk = diffsinglehunk
2817 2819 headprefixes = [
2818 2820 (b'diff', b'diff.diffline'),
2819 2821 (b'copy', b'diff.extended'),
2820 2822 (b'rename', b'diff.extended'),
2821 2823 (b'old', b'diff.extended'),
2822 2824 (b'new', b'diff.extended'),
2823 2825 (b'deleted', b'diff.extended'),
2824 2826 (b'index', b'diff.extended'),
2825 2827 (b'similarity', b'diff.extended'),
2826 2828 (b'---', b'diff.file_a'),
2827 2829 (b'+++', b'diff.file_b'),
2828 2830 ]
2829 2831 textprefixes = [
2830 2832 (b'@', b'diff.hunk'),
2831 2833 # - and + are handled by diffsinglehunk
2832 2834 ]
2833 2835 head = False
2834 2836
2835 2837 # buffers a hunk, i.e. adjacent "-", "+" lines without other changes.
2836 2838 hunkbuffer = []
2837 2839
2838 2840 def consumehunkbuffer():
2839 2841 if hunkbuffer:
2840 2842 for token in dodiffhunk(hunkbuffer):
2841 2843 yield token
2842 2844 hunkbuffer[:] = []
2843 2845
2844 2846 for chunk in func(*args, **kw):
2845 2847 lines = chunk.split(b'\n')
2846 2848 linecount = len(lines)
2847 2849 for i, line in enumerate(lines):
2848 2850 if head:
2849 2851 if line.startswith(b'@'):
2850 2852 head = False
2851 2853 else:
2852 2854 if line and not line.startswith(
2853 2855 (b' ', b'+', b'-', b'@', b'\\')
2854 2856 ):
2855 2857 head = True
2856 2858 diffline = False
2857 2859 if not head and line and line.startswith((b'+', b'-')):
2858 2860 diffline = True
2859 2861
2860 2862 prefixes = textprefixes
2861 2863 if head:
2862 2864 prefixes = headprefixes
2863 2865 if diffline:
2864 2866 # buffered
2865 2867 bufferedline = line
2866 2868 if i + 1 < linecount:
2867 2869 bufferedline += b"\n"
2868 2870 hunkbuffer.append(bufferedline)
2869 2871 else:
2870 2872 # unbuffered
2871 2873 for token in consumehunkbuffer():
2872 2874 yield token
2873 2875 stripline = line.rstrip()
2874 2876 for prefix, label in prefixes:
2875 2877 if stripline.startswith(prefix):
2876 2878 yield (stripline, label)
2877 2879 if line != stripline:
2878 2880 yield (
2879 2881 line[len(stripline) :],
2880 2882 b'diff.trailingwhitespace',
2881 2883 )
2882 2884 break
2883 2885 else:
2884 2886 yield (line, b'')
2885 2887 if i + 1 < linecount:
2886 2888 yield (b'\n', b'')
2887 2889 for token in consumehunkbuffer():
2888 2890 yield token
2889 2891
2890 2892
2891 2893 def diffui(*args, **kw):
2892 2894 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2893 2895 return difflabel(diff, *args, **kw)
2894 2896
2895 2897
2896 2898 def _filepairs(modified, added, removed, copy, opts):
2897 2899 """generates tuples (f1, f2, copyop), where f1 is the name of the file
2898 2900 before and f2 is the the name after. For added files, f1 will be None,
2899 2901 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2900 2902 or 'rename' (the latter two only if opts.git is set)."""
2901 2903 gone = set()
2902 2904
2903 2905 copyto = {v: k for k, v in copy.items()}
2904 2906
2905 2907 addedset, removedset = set(added), set(removed)
2906 2908
2907 2909 for f in sorted(modified + added + removed):
2908 2910 copyop = None
2909 2911 f1, f2 = f, f
2910 2912 if f in addedset:
2911 2913 f1 = None
2912 2914 if f in copy:
2913 2915 if opts.git:
2914 2916 f1 = copy[f]
2915 2917 if f1 in removedset and f1 not in gone:
2916 2918 copyop = b'rename'
2917 2919 gone.add(f1)
2918 2920 else:
2919 2921 copyop = b'copy'
2920 2922 elif f in removedset:
2921 2923 f2 = None
2922 2924 if opts.git:
2923 2925 # have we already reported a copy above?
2924 2926 if (
2925 2927 f in copyto
2926 2928 and copyto[f] in addedset
2927 2929 and copy[copyto[f]] == f
2928 2930 ):
2929 2931 continue
2930 2932 yield f1, f2, copyop
2931 2933
2932 2934
2933 2935 def _gitindex(text):
2934 2936 if not text:
2935 2937 text = b""
2936 2938 l = len(text)
2937 2939 s = hashutil.sha1(b'blob %d\0' % l)
2938 2940 s.update(text)
2939 2941 return hex(s.digest())
2940 2942
2941 2943
2942 2944 _gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}
2943 2945
2944 2946
2945 2947 def trydiff(
2946 2948 repo,
2947 2949 revs,
2948 2950 ctx1,
2949 2951 ctx2,
2950 2952 modified,
2951 2953 added,
2952 2954 removed,
2953 2955 copy,
2954 2956 getfilectx,
2955 2957 opts,
2956 2958 losedatafn,
2957 2959 pathfn,
2958 2960 ):
2959 2961 """given input data, generate a diff and yield it in blocks
2960 2962
2961 2963 If generating a diff would lose data like flags or binary data and
2962 2964 losedatafn is not None, it will be called.
2963 2965
2964 2966 pathfn is applied to every path in the diff output.
2965 2967 """
2966 2968
2967 2969 if opts.noprefix:
2968 2970 aprefix = bprefix = b''
2969 2971 else:
2970 2972 aprefix = b'a/'
2971 2973 bprefix = b'b/'
2972 2974
2973 2975 def diffline(f, revs):
2974 2976 revinfo = b' '.join([b"-r %s" % rev for rev in revs])
2975 2977 return b'diff %s %s' % (revinfo, f)
2976 2978
2977 2979 def isempty(fctx):
2978 2980 return fctx is None or fctx.size() == 0
2979 2981
2980 2982 date1 = dateutil.datestr(ctx1.date())
2981 2983 date2 = dateutil.datestr(ctx2.date())
2982 2984
2983 2985 if not pathfn:
2984 2986 pathfn = lambda f: f
2985 2987
2986 2988 for f1, f2, copyop in _filepairs(modified, added, removed, copy, opts):
2987 2989 content1 = None
2988 2990 content2 = None
2989 2991 fctx1 = None
2990 2992 fctx2 = None
2991 2993 flag1 = None
2992 2994 flag2 = None
2993 2995 if f1:
2994 2996 fctx1 = getfilectx(f1, ctx1)
2995 2997 if opts.git or losedatafn:
2996 2998 flag1 = ctx1.flags(f1)
2997 2999 if f2:
2998 3000 fctx2 = getfilectx(f2, ctx2)
2999 3001 if opts.git or losedatafn:
3000 3002 flag2 = ctx2.flags(f2)
3001 3003 # if binary is True, output "summary" or "base85", but not "text diff"
3002 3004 if opts.text:
3003 3005 binary = False
3004 3006 else:
3005 3007 binary = any(f.isbinary() for f in [fctx1, fctx2] if f is not None)
3006 3008
3007 3009 if losedatafn and not opts.git:
3008 3010 if (
3009 3011 binary
3010 3012 or
3011 3013 # copy/rename
3012 3014 f2 in copy
3013 3015 or
3014 3016 # empty file creation
3015 3017 (not f1 and isempty(fctx2))
3016 3018 or
3017 3019 # empty file deletion
3018 3020 (isempty(fctx1) and not f2)
3019 3021 or
3020 3022 # create with flags
3021 3023 (not f1 and flag2)
3022 3024 or
3023 3025 # change flags
3024 3026 (f1 and f2 and flag1 != flag2)
3025 3027 ):
3026 3028 losedatafn(f2 or f1)
3027 3029
3028 3030 path1 = pathfn(f1 or f2)
3029 3031 path2 = pathfn(f2 or f1)
3030 3032 header = []
3031 3033 if opts.git:
3032 3034 header.append(
3033 3035 b'diff --git %s%s %s%s' % (aprefix, path1, bprefix, path2)
3034 3036 )
3035 3037 if not f1: # added
3036 3038 header.append(b'new file mode %s' % _gitmode[flag2])
3037 3039 elif not f2: # removed
3038 3040 header.append(b'deleted file mode %s' % _gitmode[flag1])
3039 3041 else: # modified/copied/renamed
3040 3042 mode1, mode2 = _gitmode[flag1], _gitmode[flag2]
3041 3043 if mode1 != mode2:
3042 3044 header.append(b'old mode %s' % mode1)
3043 3045 header.append(b'new mode %s' % mode2)
3044 3046 if copyop is not None:
3045 3047 if opts.showsimilarity:
3046 3048 sim = similar.score(ctx1[path1], ctx2[path2]) * 100
3047 3049 header.append(b'similarity index %d%%' % sim)
3048 3050 header.append(b'%s from %s' % (copyop, path1))
3049 3051 header.append(b'%s to %s' % (copyop, path2))
3050 3052 elif revs:
3051 3053 header.append(diffline(path1, revs))
3052 3054
3053 3055 # fctx.is | diffopts | what to | is fctx.data()
3054 3056 # binary() | text nobinary git index | output? | outputted?
3055 3057 # ------------------------------------|----------------------------
3056 3058 # yes | no no no * | summary | no
3057 3059 # yes | no no yes * | base85 | yes
3058 3060 # yes | no yes no * | summary | no
3059 3061 # yes | no yes yes 0 | summary | no
3060 3062 # yes | no yes yes >0 | summary | semi [1]
3061 3063 # yes | yes * * * | text diff | yes
3062 3064 # no | * * * * | text diff | yes
3063 3065 # [1]: hash(fctx.data()) is outputted. so fctx.data() cannot be faked
3064 3066 if binary and (
3065 3067 not opts.git or (opts.git and opts.nobinary and not opts.index)
3066 3068 ):
3067 3069 # fast path: no binary content will be displayed, content1 and
3068 3070 # content2 are only used for equivalent test. cmp() could have a
3069 3071 # fast path.
3070 3072 if fctx1 is not None:
3071 3073 content1 = b'\0'
3072 3074 if fctx2 is not None:
3073 3075 if fctx1 is not None and not fctx1.cmp(fctx2):
3074 3076 content2 = b'\0' # not different
3075 3077 else:
3076 3078 content2 = b'\0\0'
3077 3079 else:
3078 3080 # normal path: load contents
3079 3081 if fctx1 is not None:
3080 3082 content1 = fctx1.data()
3081 3083 if fctx2 is not None:
3082 3084 content2 = fctx2.data()
3083 3085
3084 3086 data1 = (ctx1, fctx1, path1, flag1, content1, date1)
3085 3087 data2 = (ctx2, fctx2, path2, flag2, content2, date2)
3086 3088 yield diffcontent(data1, data2, header, binary, opts)
3087 3089
3088 3090
3089 3091 def diffcontent(data1, data2, header, binary, opts):
3090 3092 """diffs two versions of a file.
3091 3093
3092 3094 data1 and data2 are tuples containg:
3093 3095
3094 3096 * ctx: changeset for the file
3095 3097 * fctx: file context for that file
3096 3098 * path1: name of the file
3097 3099 * flag: flags of the file
3098 3100 * content: full content of the file (can be null in case of binary)
3099 3101 * date: date of the changeset
3100 3102
3101 3103 header: the patch header
3102 3104 binary: whether the any of the version of file is binary or not
3103 3105 opts: user passed options
3104 3106
3105 3107 It exists as a separate function so that extensions like extdiff can wrap
3106 3108 it and use the file content directly.
3107 3109 """
3108 3110
3109 3111 ctx1, fctx1, path1, flag1, content1, date1 = data1
3110 3112 ctx2, fctx2, path2, flag2, content2, date2 = data2
3111 3113 index1 = _gitindex(content1) if path1 in ctx1 else sha1nodeconstants.nullhex
3112 3114 index2 = _gitindex(content2) if path2 in ctx2 else sha1nodeconstants.nullhex
3113 3115 if binary and opts.git and not opts.nobinary:
3114 3116 text = mdiff.b85diff(content1, content2)
3115 3117 if text:
3116 3118 header.append(b'index %s..%s' % (index1, index2))
3117 3119 hunks = ((None, [text]),)
3118 3120 else:
3119 3121 if opts.git and opts.index > 0:
3120 3122 flag = flag1
3121 3123 if flag is None:
3122 3124 flag = flag2
3123 3125 header.append(
3124 3126 b'index %s..%s %s'
3125 3127 % (
3126 3128 index1[0 : opts.index],
3127 3129 index2[0 : opts.index],
3128 3130 _gitmode[flag],
3129 3131 )
3130 3132 )
3131 3133
3132 3134 uheaders, hunks = mdiff.unidiff(
3133 3135 content1,
3134 3136 date1,
3135 3137 content2,
3136 3138 date2,
3137 3139 path1,
3138 3140 path2,
3139 3141 binary=binary,
3140 3142 opts=opts,
3141 3143 )
3142 3144 header.extend(uheaders)
3143 3145 return fctx1, fctx2, header, hunks
3144 3146
3145 3147
3146 3148 def diffstatsum(stats):
3147 3149 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
3148 3150 for f, a, r, b in stats:
3149 3151 maxfile = max(maxfile, encoding.colwidth(f))
3150 3152 maxtotal = max(maxtotal, a + r)
3151 3153 addtotal += a
3152 3154 removetotal += r
3153 3155 binary = binary or b
3154 3156
3155 3157 return maxfile, maxtotal, addtotal, removetotal, binary
3156 3158
3157 3159
3158 3160 def diffstatdata(lines):
3159 3161 diffre = re.compile(br'^diff .*-r [a-z0-9]+\s(.*)$')
3160 3162
3161 3163 results = []
3162 3164 filename, adds, removes, isbinary = None, 0, 0, False
3163 3165
3164 3166 def addresult():
3165 3167 if filename:
3166 3168 results.append((filename, adds, removes, isbinary))
3167 3169
3168 3170 # inheader is used to track if a line is in the
3169 3171 # header portion of the diff. This helps properly account
3170 3172 # for lines that start with '--' or '++'
3171 3173 inheader = False
3172 3174
3173 3175 for line in lines:
3174 3176 if line.startswith(b'diff'):
3175 3177 addresult()
3176 3178 # starting a new file diff
3177 3179 # set numbers to 0 and reset inheader
3178 3180 inheader = True
3179 3181 adds, removes, isbinary = 0, 0, False
3180 3182 if line.startswith(b'diff --git a/'):
3181 3183 filename = gitre.search(line).group(2)
3182 3184 elif line.startswith(b'diff -r'):
3183 3185 # format: "diff -r ... -r ... filename"
3184 3186 filename = diffre.search(line).group(1)
3185 3187 elif line.startswith(b'@@'):
3186 3188 inheader = False
3187 3189 elif line.startswith(b'+') and not inheader:
3188 3190 adds += 1
3189 3191 elif line.startswith(b'-') and not inheader:
3190 3192 removes += 1
3191 3193 elif line.startswith(b'GIT binary patch') or line.startswith(
3192 3194 b'Binary file'
3193 3195 ):
3194 3196 isbinary = True
3195 3197 elif line.startswith(b'rename from'):
3196 3198 filename = line[12:]
3197 3199 elif line.startswith(b'rename to'):
3198 3200 filename += b' => %s' % line[10:]
3199 3201 addresult()
3200 3202 return results
3201 3203
3202 3204
3203 3205 def diffstat(lines, width=80):
3204 3206 output = []
3205 3207 stats = diffstatdata(lines)
3206 3208 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
3207 3209
3208 3210 countwidth = len(str(maxtotal))
3209 3211 if hasbinary and countwidth < 3:
3210 3212 countwidth = 3
3211 3213 graphwidth = width - countwidth - maxname - 6
3212 3214 if graphwidth < 10:
3213 3215 graphwidth = 10
3214 3216
3215 3217 def scale(i):
3216 3218 if maxtotal <= graphwidth:
3217 3219 return i
3218 3220 # If diffstat runs out of room it doesn't print anything,
3219 3221 # which isn't very useful, so always print at least one + or -
3220 3222 # if there were at least some changes.
3221 3223 return max(i * graphwidth // maxtotal, int(bool(i)))
3222 3224
3223 3225 for filename, adds, removes, isbinary in stats:
3224 3226 if isbinary:
3225 3227 count = b'Bin'
3226 3228 else:
3227 3229 count = b'%d' % (adds + removes)
3228 3230 pluses = b'+' * scale(adds)
3229 3231 minuses = b'-' * scale(removes)
3230 3232 output.append(
3231 3233 b' %s%s | %*s %s%s\n'
3232 3234 % (
3233 3235 filename,
3234 3236 b' ' * (maxname - encoding.colwidth(filename)),
3235 3237 countwidth,
3236 3238 count,
3237 3239 pluses,
3238 3240 minuses,
3239 3241 )
3240 3242 )
3241 3243
3242 3244 if stats:
3243 3245 output.append(
3244 3246 _(b' %d files changed, %d insertions(+), %d deletions(-)\n')
3245 3247 % (len(stats), totaladds, totalremoves)
3246 3248 )
3247 3249
3248 3250 return b''.join(output)
3249 3251
3250 3252
3251 3253 def diffstatui(*args, **kw):
3252 3254 """like diffstat(), but yields 2-tuples of (output, label) for
3253 3255 ui.write()
3254 3256 """
3255 3257
3256 3258 for line in diffstat(*args, **kw).splitlines():
3257 3259 if line and line[-1] in b'+-':
3258 3260 name, graph = line.rsplit(b' ', 1)
3259 3261 yield (name + b' ', b'')
3260 3262 m = re.search(br'\++', graph)
3261 3263 if m:
3262 3264 yield (m.group(0), b'diffstat.inserted')
3263 3265 m = re.search(br'-+', graph)
3264 3266 if m:
3265 3267 yield (m.group(0), b'diffstat.deleted')
3266 3268 else:
3267 3269 yield (line, b'')
3268 3270 yield (b'\n', b'')
@@ -1,88 +1,86 b''
1 1 #require pytype py3 slow
2 2
3 3 $ cd $RUNTESTDIR/..
4 4
5 5 Many of the individual files that are excluded here confuse pytype
6 6 because they do a mix of Python 2 and Python 3 things
7 7 conditionally. There's no good way to help it out with that as far as
8 8 I can tell, so let's just hide those files from it for now. We should
9 9 endeavor to empty this list out over time, as some of these are
10 10 probably hiding real problems.
11 11
12 12 mercurial/bundlerepo.py # no vfs and ui attrs on bundlerepo
13 13 mercurial/chgserver.py # [attribute-error]
14 14 mercurial/context.py # many [attribute-error]
15 15 mercurial/crecord.py # tons of [attribute-error], [module-attr]
16 16 mercurial/debugcommands.py # [wrong-arg-types]
17 17 mercurial/dispatch.py # initstdio: No attribute ... on TextIO [attribute-error]
18 18 mercurial/exchange.py # [attribute-error]
19 19 mercurial/hgweb/hgweb_mod.py # [attribute-error], [name-error], [wrong-arg-types]
20 20 mercurial/hgweb/server.py # [attribute-error], [name-error], [module-attr]
21 21 mercurial/hgweb/webcommands.py # [missing-parameter]
22 22 mercurial/hgweb/wsgicgi.py # confused values in os.environ
23 23 mercurial/httppeer.py # [attribute-error], [wrong-arg-types]
24 24 mercurial/interfaces # No attribute 'capabilities' on peer [attribute-error]
25 25 mercurial/keepalive.py # [attribute-error]
26 26 mercurial/localrepo.py # [attribute-error]
27 27 mercurial/manifest.py # [unsupported-operands], [wrong-arg-types]
28 28 mercurial/minirst.py # [unsupported-operands], [attribute-error]
29 mercurial/patch.py # [wrong-arg-types]
30 29 mercurial/pure/osutil.py # [invalid-typevar], [not-callable]
31 30 mercurial/pure/parsers.py # [attribute-error]
32 31 mercurial/pycompat.py # bytes vs str issues
33 32 mercurial/repoview.py # [attribute-error]
34 33 mercurial/sslutil.py # [attribute-error]
35 34 mercurial/testing/storage.py # tons of [attribute-error]
36 35 mercurial/ui.py # [attribute-error], [wrong-arg-types]
37 36 mercurial/unionrepo.py # ui, svfs, unfiltered [attribute-error]
38 37 mercurial/util.py # [attribute-error], [wrong-arg-count]
39 38 mercurial/utils/procutil.py # [attribute-error], [module-attr], [bad-return-type]
40 39 mercurial/utils/memorytop.py # not 3.6 compatible
41 40 mercurial/win32.py # [not-callable]
42 41 mercurial/wireprotoframing.py # [unsupported-operands], [attribute-error], [import-error]
43 42 mercurial/wireprotoserver.py # line 253, in _availableapis: No attribute '__iter__' on Callable[[Any, Any], Any] [attribute-error]
44 43 mercurial/wireprotov1peer.py # [attribute-error]
45 44 mercurial/wireprotov1server.py # BUG?: BundleValueError handler accesses subclass's attrs
46 45
47 46 TODO: use --no-cache on test server? Caching the files locally helps during
48 47 development, but may be a hinderance for CI testing.
49 48
50 49 $ pytype -V 3.6 --keep-going --jobs auto mercurial \
51 50 > -x mercurial/bundlerepo.py \
52 51 > -x mercurial/chgserver.py \
53 52 > -x mercurial/context.py \
54 53 > -x mercurial/crecord.py \
55 54 > -x mercurial/debugcommands.py \
56 55 > -x mercurial/dispatch.py \
57 56 > -x mercurial/exchange.py \
58 57 > -x mercurial/hgweb/hgweb_mod.py \
59 58 > -x mercurial/hgweb/server.py \
60 59 > -x mercurial/hgweb/webcommands.py \
61 60 > -x mercurial/hgweb/wsgicgi.py \
62 61 > -x mercurial/httppeer.py \
63 62 > -x mercurial/interfaces \
64 63 > -x mercurial/keepalive.py \
65 64 > -x mercurial/localrepo.py \
66 65 > -x mercurial/manifest.py \
67 66 > -x mercurial/minirst.py \
68 > -x mercurial/patch.py \
69 67 > -x mercurial/pure/osutil.py \
70 68 > -x mercurial/pure/parsers.py \
71 69 > -x mercurial/pycompat.py \
72 70 > -x mercurial/repoview.py \
73 71 > -x mercurial/sslutil.py \
74 72 > -x mercurial/testing/storage.py \
75 73 > -x mercurial/thirdparty \
76 74 > -x mercurial/ui.py \
77 75 > -x mercurial/unionrepo.py \
78 76 > -x mercurial/utils/procutil.py \
79 77 > -x mercurial/utils/memorytop.py \
80 78 > -x mercurial/win32.py \
81 79 > -x mercurial/wireprotoframing.py \
82 80 > -x mercurial/wireprotoserver.py \
83 81 > -x mercurial/wireprotov1peer.py \
84 82 > -x mercurial/wireprotov1server.py \
85 83 > > $TESTTMP/pytype-output.txt || cat $TESTTMP/pytype-output.txt
86 84
87 85 Only show the results on a failure, because the output on success is also
88 86 voluminous and variable.
General Comments 0
You need to be logged in to leave comments. Login now