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