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