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