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