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