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