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