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