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