##// END OF EJS Templates
patch: enable diff.tab markup for the color extension...
Jordi Gutiérrez Hermoso -
r22460:c343557a default
parent child Browse files
Show More
@@ -1,1933 +1,1945 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 import cStringIO, email, os, errno, re, posixpath
10 10 import tempfile, zlib, shutil
11 11 # On python2.4 you have to import these by name or they fail to
12 12 # load. This was not a problem on Python 2.7.
13 13 import email.Generator
14 14 import email.Parser
15 15
16 16 from i18n import _
17 17 from node import hex, short
18 18 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
19 19
20 20 gitre = re.compile('diff --git a/(.*) b/(.*)')
21 tabsplitter = re.compile(r'(\t+|[^\t]+)')
21 22
22 23 class PatchError(Exception):
23 24 pass
24 25
25 26
26 27 # public functions
27 28
28 29 def split(stream):
29 30 '''return an iterator of individual patches from a stream'''
30 31 def isheader(line, inheader):
31 32 if inheader and line[0] in (' ', '\t'):
32 33 # continuation
33 34 return True
34 35 if line[0] in (' ', '-', '+'):
35 36 # diff line - don't check for header pattern in there
36 37 return False
37 38 l = line.split(': ', 1)
38 39 return len(l) == 2 and ' ' not in l[0]
39 40
40 41 def chunk(lines):
41 42 return cStringIO.StringIO(''.join(lines))
42 43
43 44 def hgsplit(stream, cur):
44 45 inheader = True
45 46
46 47 for line in stream:
47 48 if not line.strip():
48 49 inheader = False
49 50 if not inheader and line.startswith('# HG changeset patch'):
50 51 yield chunk(cur)
51 52 cur = []
52 53 inheader = True
53 54
54 55 cur.append(line)
55 56
56 57 if cur:
57 58 yield chunk(cur)
58 59
59 60 def mboxsplit(stream, cur):
60 61 for line in stream:
61 62 if line.startswith('From '):
62 63 for c in split(chunk(cur[1:])):
63 64 yield c
64 65 cur = []
65 66
66 67 cur.append(line)
67 68
68 69 if cur:
69 70 for c in split(chunk(cur[1:])):
70 71 yield c
71 72
72 73 def mimesplit(stream, cur):
73 74 def msgfp(m):
74 75 fp = cStringIO.StringIO()
75 76 g = email.Generator.Generator(fp, mangle_from_=False)
76 77 g.flatten(m)
77 78 fp.seek(0)
78 79 return fp
79 80
80 81 for line in stream:
81 82 cur.append(line)
82 83 c = chunk(cur)
83 84
84 85 m = email.Parser.Parser().parse(c)
85 86 if not m.is_multipart():
86 87 yield msgfp(m)
87 88 else:
88 89 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
89 90 for part in m.walk():
90 91 ct = part.get_content_type()
91 92 if ct not in ok_types:
92 93 continue
93 94 yield msgfp(part)
94 95
95 96 def headersplit(stream, cur):
96 97 inheader = False
97 98
98 99 for line in stream:
99 100 if not inheader and isheader(line, inheader):
100 101 yield chunk(cur)
101 102 cur = []
102 103 inheader = True
103 104 if inheader and not isheader(line, inheader):
104 105 inheader = False
105 106
106 107 cur.append(line)
107 108
108 109 if cur:
109 110 yield chunk(cur)
110 111
111 112 def remainder(cur):
112 113 yield chunk(cur)
113 114
114 115 class fiter(object):
115 116 def __init__(self, fp):
116 117 self.fp = fp
117 118
118 119 def __iter__(self):
119 120 return self
120 121
121 122 def next(self):
122 123 l = self.fp.readline()
123 124 if not l:
124 125 raise StopIteration
125 126 return l
126 127
127 128 inheader = False
128 129 cur = []
129 130
130 131 mimeheaders = ['content-type']
131 132
132 133 if not util.safehasattr(stream, 'next'):
133 134 # http responses, for example, have readline but not next
134 135 stream = fiter(stream)
135 136
136 137 for line in stream:
137 138 cur.append(line)
138 139 if line.startswith('# HG changeset patch'):
139 140 return hgsplit(stream, cur)
140 141 elif line.startswith('From '):
141 142 return mboxsplit(stream, cur)
142 143 elif isheader(line, inheader):
143 144 inheader = True
144 145 if line.split(':', 1)[0].lower() in mimeheaders:
145 146 # let email parser handle this
146 147 return mimesplit(stream, cur)
147 148 elif line.startswith('--- ') and inheader:
148 149 # No evil headers seen by diff start, split by hand
149 150 return headersplit(stream, cur)
150 151 # Not enough info, keep reading
151 152
152 153 # if we are here, we have a very plain patch
153 154 return remainder(cur)
154 155
155 156 def extract(ui, fileobj):
156 157 '''extract patch from data read from fileobj.
157 158
158 159 patch can be a normal patch or contained in an email message.
159 160
160 161 return tuple (filename, message, user, date, branch, node, p1, p2).
161 162 Any item in the returned tuple can be None. If filename is None,
162 163 fileobj did not contain a patch. Caller must unlink filename when done.'''
163 164
164 165 # attempt to detect the start of a patch
165 166 # (this heuristic is borrowed from quilt)
166 167 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
167 168 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
168 169 r'---[ \t].*?^\+\+\+[ \t]|'
169 170 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
170 171
171 172 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
172 173 tmpfp = os.fdopen(fd, 'w')
173 174 try:
174 175 msg = email.Parser.Parser().parse(fileobj)
175 176
176 177 subject = msg['Subject']
177 178 user = msg['From']
178 179 if not subject and not user:
179 180 # Not an email, restore parsed headers if any
180 181 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
181 182
182 183 # should try to parse msg['Date']
183 184 date = None
184 185 nodeid = None
185 186 branch = None
186 187 parents = []
187 188
188 189 if subject:
189 190 if subject.startswith('[PATCH'):
190 191 pend = subject.find(']')
191 192 if pend >= 0:
192 193 subject = subject[pend + 1:].lstrip()
193 194 subject = re.sub(r'\n[ \t]+', ' ', subject)
194 195 ui.debug('Subject: %s\n' % subject)
195 196 if user:
196 197 ui.debug('From: %s\n' % user)
197 198 diffs_seen = 0
198 199 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
199 200 message = ''
200 201 for part in msg.walk():
201 202 content_type = part.get_content_type()
202 203 ui.debug('Content-Type: %s\n' % content_type)
203 204 if content_type not in ok_types:
204 205 continue
205 206 payload = part.get_payload(decode=True)
206 207 m = diffre.search(payload)
207 208 if m:
208 209 hgpatch = False
209 210 hgpatchheader = False
210 211 ignoretext = False
211 212
212 213 ui.debug('found patch at byte %d\n' % m.start(0))
213 214 diffs_seen += 1
214 215 cfp = cStringIO.StringIO()
215 216 for line in payload[:m.start(0)].splitlines():
216 217 if line.startswith('# HG changeset patch') and not hgpatch:
217 218 ui.debug('patch generated by hg export\n')
218 219 hgpatch = True
219 220 hgpatchheader = True
220 221 # drop earlier commit message content
221 222 cfp.seek(0)
222 223 cfp.truncate()
223 224 subject = None
224 225 elif hgpatchheader:
225 226 if line.startswith('# User '):
226 227 user = line[7:]
227 228 ui.debug('From: %s\n' % user)
228 229 elif line.startswith("# Date "):
229 230 date = line[7:]
230 231 elif line.startswith("# Branch "):
231 232 branch = line[9:]
232 233 elif line.startswith("# Node ID "):
233 234 nodeid = line[10:]
234 235 elif line.startswith("# Parent "):
235 236 parents.append(line[9:].lstrip())
236 237 elif not line.startswith("# "):
237 238 hgpatchheader = False
238 239 elif line == '---':
239 240 ignoretext = True
240 241 if not hgpatchheader and not ignoretext:
241 242 cfp.write(line)
242 243 cfp.write('\n')
243 244 message = cfp.getvalue()
244 245 if tmpfp:
245 246 tmpfp.write(payload)
246 247 if not payload.endswith('\n'):
247 248 tmpfp.write('\n')
248 249 elif not diffs_seen and message and content_type == 'text/plain':
249 250 message += '\n' + payload
250 251 except: # re-raises
251 252 tmpfp.close()
252 253 os.unlink(tmpname)
253 254 raise
254 255
255 256 if subject and not message.startswith(subject):
256 257 message = '%s\n%s' % (subject, message)
257 258 tmpfp.close()
258 259 if not diffs_seen:
259 260 os.unlink(tmpname)
260 261 return None, message, user, date, branch, None, None, None
261 262 p1 = parents and parents.pop(0) or None
262 263 p2 = parents and parents.pop(0) or None
263 264 return tmpname, message, user, date, branch, nodeid, p1, p2
264 265
265 266 class patchmeta(object):
266 267 """Patched file metadata
267 268
268 269 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
269 270 or COPY. 'path' is patched file path. 'oldpath' is set to the
270 271 origin file when 'op' is either COPY or RENAME, None otherwise. If
271 272 file mode is changed, 'mode' is a tuple (islink, isexec) where
272 273 'islink' is True if the file is a symlink and 'isexec' is True if
273 274 the file is executable. Otherwise, 'mode' is None.
274 275 """
275 276 def __init__(self, path):
276 277 self.path = path
277 278 self.oldpath = None
278 279 self.mode = None
279 280 self.op = 'MODIFY'
280 281 self.binary = False
281 282
282 283 def setmode(self, mode):
283 284 islink = mode & 020000
284 285 isexec = mode & 0100
285 286 self.mode = (islink, isexec)
286 287
287 288 def copy(self):
288 289 other = patchmeta(self.path)
289 290 other.oldpath = self.oldpath
290 291 other.mode = self.mode
291 292 other.op = self.op
292 293 other.binary = self.binary
293 294 return other
294 295
295 296 def _ispatchinga(self, afile):
296 297 if afile == '/dev/null':
297 298 return self.op == 'ADD'
298 299 return afile == 'a/' + (self.oldpath or self.path)
299 300
300 301 def _ispatchingb(self, bfile):
301 302 if bfile == '/dev/null':
302 303 return self.op == 'DELETE'
303 304 return bfile == 'b/' + self.path
304 305
305 306 def ispatching(self, afile, bfile):
306 307 return self._ispatchinga(afile) and self._ispatchingb(bfile)
307 308
308 309 def __repr__(self):
309 310 return "<patchmeta %s %r>" % (self.op, self.path)
310 311
311 312 def readgitpatch(lr):
312 313 """extract git-style metadata about patches from <patchname>"""
313 314
314 315 # Filter patch for git information
315 316 gp = None
316 317 gitpatches = []
317 318 for line in lr:
318 319 line = line.rstrip(' \r\n')
319 320 if line.startswith('diff --git a/'):
320 321 m = gitre.match(line)
321 322 if m:
322 323 if gp:
323 324 gitpatches.append(gp)
324 325 dst = m.group(2)
325 326 gp = patchmeta(dst)
326 327 elif gp:
327 328 if line.startswith('--- '):
328 329 gitpatches.append(gp)
329 330 gp = None
330 331 continue
331 332 if line.startswith('rename from '):
332 333 gp.op = 'RENAME'
333 334 gp.oldpath = line[12:]
334 335 elif line.startswith('rename to '):
335 336 gp.path = line[10:]
336 337 elif line.startswith('copy from '):
337 338 gp.op = 'COPY'
338 339 gp.oldpath = line[10:]
339 340 elif line.startswith('copy to '):
340 341 gp.path = line[8:]
341 342 elif line.startswith('deleted file'):
342 343 gp.op = 'DELETE'
343 344 elif line.startswith('new file mode '):
344 345 gp.op = 'ADD'
345 346 gp.setmode(int(line[-6:], 8))
346 347 elif line.startswith('new mode '):
347 348 gp.setmode(int(line[-6:], 8))
348 349 elif line.startswith('GIT binary patch'):
349 350 gp.binary = True
350 351 if gp:
351 352 gitpatches.append(gp)
352 353
353 354 return gitpatches
354 355
355 356 class linereader(object):
356 357 # simple class to allow pushing lines back into the input stream
357 358 def __init__(self, fp):
358 359 self.fp = fp
359 360 self.buf = []
360 361
361 362 def push(self, line):
362 363 if line is not None:
363 364 self.buf.append(line)
364 365
365 366 def readline(self):
366 367 if self.buf:
367 368 l = self.buf[0]
368 369 del self.buf[0]
369 370 return l
370 371 return self.fp.readline()
371 372
372 373 def __iter__(self):
373 374 while True:
374 375 l = self.readline()
375 376 if not l:
376 377 break
377 378 yield l
378 379
379 380 class abstractbackend(object):
380 381 def __init__(self, ui):
381 382 self.ui = ui
382 383
383 384 def getfile(self, fname):
384 385 """Return target file data and flags as a (data, (islink,
385 386 isexec)) tuple. Data is None if file is missing/deleted.
386 387 """
387 388 raise NotImplementedError
388 389
389 390 def setfile(self, fname, data, mode, copysource):
390 391 """Write data to target file fname and set its mode. mode is a
391 392 (islink, isexec) tuple. If data is None, the file content should
392 393 be left unchanged. If the file is modified after being copied,
393 394 copysource is set to the original file name.
394 395 """
395 396 raise NotImplementedError
396 397
397 398 def unlink(self, fname):
398 399 """Unlink target file."""
399 400 raise NotImplementedError
400 401
401 402 def writerej(self, fname, failed, total, lines):
402 403 """Write rejected lines for fname. total is the number of hunks
403 404 which failed to apply and total the total number of hunks for this
404 405 files.
405 406 """
406 407 pass
407 408
408 409 def exists(self, fname):
409 410 raise NotImplementedError
410 411
411 412 class fsbackend(abstractbackend):
412 413 def __init__(self, ui, basedir):
413 414 super(fsbackend, self).__init__(ui)
414 415 self.opener = scmutil.opener(basedir)
415 416
416 417 def _join(self, f):
417 418 return os.path.join(self.opener.base, f)
418 419
419 420 def getfile(self, fname):
420 421 if self.opener.islink(fname):
421 422 return (self.opener.readlink(fname), (True, False))
422 423
423 424 isexec = False
424 425 try:
425 426 isexec = self.opener.lstat(fname).st_mode & 0100 != 0
426 427 except OSError, e:
427 428 if e.errno != errno.ENOENT:
428 429 raise
429 430 try:
430 431 return (self.opener.read(fname), (False, isexec))
431 432 except IOError, e:
432 433 if e.errno != errno.ENOENT:
433 434 raise
434 435 return None, None
435 436
436 437 def setfile(self, fname, data, mode, copysource):
437 438 islink, isexec = mode
438 439 if data is None:
439 440 self.opener.setflags(fname, islink, isexec)
440 441 return
441 442 if islink:
442 443 self.opener.symlink(data, fname)
443 444 else:
444 445 self.opener.write(fname, data)
445 446 if isexec:
446 447 self.opener.setflags(fname, False, True)
447 448
448 449 def unlink(self, fname):
449 450 self.opener.unlinkpath(fname, ignoremissing=True)
450 451
451 452 def writerej(self, fname, failed, total, lines):
452 453 fname = fname + ".rej"
453 454 self.ui.warn(
454 455 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
455 456 (failed, total, fname))
456 457 fp = self.opener(fname, 'w')
457 458 fp.writelines(lines)
458 459 fp.close()
459 460
460 461 def exists(self, fname):
461 462 return self.opener.lexists(fname)
462 463
463 464 class workingbackend(fsbackend):
464 465 def __init__(self, ui, repo, similarity):
465 466 super(workingbackend, self).__init__(ui, repo.root)
466 467 self.repo = repo
467 468 self.similarity = similarity
468 469 self.removed = set()
469 470 self.changed = set()
470 471 self.copied = []
471 472
472 473 def _checkknown(self, fname):
473 474 if self.repo.dirstate[fname] == '?' and self.exists(fname):
474 475 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
475 476
476 477 def setfile(self, fname, data, mode, copysource):
477 478 self._checkknown(fname)
478 479 super(workingbackend, self).setfile(fname, data, mode, copysource)
479 480 if copysource is not None:
480 481 self.copied.append((copysource, fname))
481 482 self.changed.add(fname)
482 483
483 484 def unlink(self, fname):
484 485 self._checkknown(fname)
485 486 super(workingbackend, self).unlink(fname)
486 487 self.removed.add(fname)
487 488 self.changed.add(fname)
488 489
489 490 def close(self):
490 491 wctx = self.repo[None]
491 492 changed = set(self.changed)
492 493 for src, dst in self.copied:
493 494 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
494 495 if self.removed:
495 496 wctx.forget(sorted(self.removed))
496 497 for f in self.removed:
497 498 if f not in self.repo.dirstate:
498 499 # File was deleted and no longer belongs to the
499 500 # dirstate, it was probably marked added then
500 501 # deleted, and should not be considered by
501 502 # marktouched().
502 503 changed.discard(f)
503 504 if changed:
504 505 scmutil.marktouched(self.repo, changed, self.similarity)
505 506 return sorted(self.changed)
506 507
507 508 class filestore(object):
508 509 def __init__(self, maxsize=None):
509 510 self.opener = None
510 511 self.files = {}
511 512 self.created = 0
512 513 self.maxsize = maxsize
513 514 if self.maxsize is None:
514 515 self.maxsize = 4*(2**20)
515 516 self.size = 0
516 517 self.data = {}
517 518
518 519 def setfile(self, fname, data, mode, copied=None):
519 520 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
520 521 self.data[fname] = (data, mode, copied)
521 522 self.size += len(data)
522 523 else:
523 524 if self.opener is None:
524 525 root = tempfile.mkdtemp(prefix='hg-patch-')
525 526 self.opener = scmutil.opener(root)
526 527 # Avoid filename issues with these simple names
527 528 fn = str(self.created)
528 529 self.opener.write(fn, data)
529 530 self.created += 1
530 531 self.files[fname] = (fn, mode, copied)
531 532
532 533 def getfile(self, fname):
533 534 if fname in self.data:
534 535 return self.data[fname]
535 536 if not self.opener or fname not in self.files:
536 537 return None, None, None
537 538 fn, mode, copied = self.files[fname]
538 539 return self.opener.read(fn), mode, copied
539 540
540 541 def close(self):
541 542 if self.opener:
542 543 shutil.rmtree(self.opener.base)
543 544
544 545 class repobackend(abstractbackend):
545 546 def __init__(self, ui, repo, ctx, store):
546 547 super(repobackend, self).__init__(ui)
547 548 self.repo = repo
548 549 self.ctx = ctx
549 550 self.store = store
550 551 self.changed = set()
551 552 self.removed = set()
552 553 self.copied = {}
553 554
554 555 def _checkknown(self, fname):
555 556 if fname not in self.ctx:
556 557 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
557 558
558 559 def getfile(self, fname):
559 560 try:
560 561 fctx = self.ctx[fname]
561 562 except error.LookupError:
562 563 return None, None
563 564 flags = fctx.flags()
564 565 return fctx.data(), ('l' in flags, 'x' in flags)
565 566
566 567 def setfile(self, fname, data, mode, copysource):
567 568 if copysource:
568 569 self._checkknown(copysource)
569 570 if data is None:
570 571 data = self.ctx[fname].data()
571 572 self.store.setfile(fname, data, mode, copysource)
572 573 self.changed.add(fname)
573 574 if copysource:
574 575 self.copied[fname] = copysource
575 576
576 577 def unlink(self, fname):
577 578 self._checkknown(fname)
578 579 self.removed.add(fname)
579 580
580 581 def exists(self, fname):
581 582 return fname in self.ctx
582 583
583 584 def close(self):
584 585 return self.changed | self.removed
585 586
586 587 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
587 588 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
588 589 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
589 590 eolmodes = ['strict', 'crlf', 'lf', 'auto']
590 591
591 592 class patchfile(object):
592 593 def __init__(self, ui, gp, backend, store, eolmode='strict'):
593 594 self.fname = gp.path
594 595 self.eolmode = eolmode
595 596 self.eol = None
596 597 self.backend = backend
597 598 self.ui = ui
598 599 self.lines = []
599 600 self.exists = False
600 601 self.missing = True
601 602 self.mode = gp.mode
602 603 self.copysource = gp.oldpath
603 604 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
604 605 self.remove = gp.op == 'DELETE'
605 606 if self.copysource is None:
606 607 data, mode = backend.getfile(self.fname)
607 608 else:
608 609 data, mode = store.getfile(self.copysource)[:2]
609 610 if data is not None:
610 611 self.exists = self.copysource is None or backend.exists(self.fname)
611 612 self.missing = False
612 613 if data:
613 614 self.lines = mdiff.splitnewlines(data)
614 615 if self.mode is None:
615 616 self.mode = mode
616 617 if self.lines:
617 618 # Normalize line endings
618 619 if self.lines[0].endswith('\r\n'):
619 620 self.eol = '\r\n'
620 621 elif self.lines[0].endswith('\n'):
621 622 self.eol = '\n'
622 623 if eolmode != 'strict':
623 624 nlines = []
624 625 for l in self.lines:
625 626 if l.endswith('\r\n'):
626 627 l = l[:-2] + '\n'
627 628 nlines.append(l)
628 629 self.lines = nlines
629 630 else:
630 631 if self.create:
631 632 self.missing = False
632 633 if self.mode is None:
633 634 self.mode = (False, False)
634 635 if self.missing:
635 636 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
636 637
637 638 self.hash = {}
638 639 self.dirty = 0
639 640 self.offset = 0
640 641 self.skew = 0
641 642 self.rej = []
642 643 self.fileprinted = False
643 644 self.printfile(False)
644 645 self.hunks = 0
645 646
646 647 def writelines(self, fname, lines, mode):
647 648 if self.eolmode == 'auto':
648 649 eol = self.eol
649 650 elif self.eolmode == 'crlf':
650 651 eol = '\r\n'
651 652 else:
652 653 eol = '\n'
653 654
654 655 if self.eolmode != 'strict' and eol and eol != '\n':
655 656 rawlines = []
656 657 for l in lines:
657 658 if l and l[-1] == '\n':
658 659 l = l[:-1] + eol
659 660 rawlines.append(l)
660 661 lines = rawlines
661 662
662 663 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
663 664
664 665 def printfile(self, warn):
665 666 if self.fileprinted:
666 667 return
667 668 if warn or self.ui.verbose:
668 669 self.fileprinted = True
669 670 s = _("patching file %s\n") % self.fname
670 671 if warn:
671 672 self.ui.warn(s)
672 673 else:
673 674 self.ui.note(s)
674 675
675 676
676 677 def findlines(self, l, linenum):
677 678 # looks through the hash and finds candidate lines. The
678 679 # result is a list of line numbers sorted based on distance
679 680 # from linenum
680 681
681 682 cand = self.hash.get(l, [])
682 683 if len(cand) > 1:
683 684 # resort our list of potentials forward then back.
684 685 cand.sort(key=lambda x: abs(x - linenum))
685 686 return cand
686 687
687 688 def write_rej(self):
688 689 # our rejects are a little different from patch(1). This always
689 690 # creates rejects in the same form as the original patch. A file
690 691 # header is inserted so that you can run the reject through patch again
691 692 # without having to type the filename.
692 693 if not self.rej:
693 694 return
694 695 base = os.path.basename(self.fname)
695 696 lines = ["--- %s\n+++ %s\n" % (base, base)]
696 697 for x in self.rej:
697 698 for l in x.hunk:
698 699 lines.append(l)
699 700 if l[-1] != '\n':
700 701 lines.append("\n\ No newline at end of file\n")
701 702 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
702 703
703 704 def apply(self, h):
704 705 if not h.complete():
705 706 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
706 707 (h.number, h.desc, len(h.a), h.lena, len(h.b),
707 708 h.lenb))
708 709
709 710 self.hunks += 1
710 711
711 712 if self.missing:
712 713 self.rej.append(h)
713 714 return -1
714 715
715 716 if self.exists and self.create:
716 717 if self.copysource:
717 718 self.ui.warn(_("cannot create %s: destination already "
718 719 "exists\n") % self.fname)
719 720 else:
720 721 self.ui.warn(_("file %s already exists\n") % self.fname)
721 722 self.rej.append(h)
722 723 return -1
723 724
724 725 if isinstance(h, binhunk):
725 726 if self.remove:
726 727 self.backend.unlink(self.fname)
727 728 else:
728 729 l = h.new(self.lines)
729 730 self.lines[:] = l
730 731 self.offset += len(l)
731 732 self.dirty = True
732 733 return 0
733 734
734 735 horig = h
735 736 if (self.eolmode in ('crlf', 'lf')
736 737 or self.eolmode == 'auto' and self.eol):
737 738 # If new eols are going to be normalized, then normalize
738 739 # hunk data before patching. Otherwise, preserve input
739 740 # line-endings.
740 741 h = h.getnormalized()
741 742
742 743 # fast case first, no offsets, no fuzz
743 744 old, oldstart, new, newstart = h.fuzzit(0, False)
744 745 oldstart += self.offset
745 746 orig_start = oldstart
746 747 # if there's skew we want to emit the "(offset %d lines)" even
747 748 # when the hunk cleanly applies at start + skew, so skip the
748 749 # fast case code
749 750 if (self.skew == 0 and
750 751 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
751 752 if self.remove:
752 753 self.backend.unlink(self.fname)
753 754 else:
754 755 self.lines[oldstart:oldstart + len(old)] = new
755 756 self.offset += len(new) - len(old)
756 757 self.dirty = True
757 758 return 0
758 759
759 760 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
760 761 self.hash = {}
761 762 for x, s in enumerate(self.lines):
762 763 self.hash.setdefault(s, []).append(x)
763 764
764 765 for fuzzlen in xrange(3):
765 766 for toponly in [True, False]:
766 767 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
767 768 oldstart = oldstart + self.offset + self.skew
768 769 oldstart = min(oldstart, len(self.lines))
769 770 if old:
770 771 cand = self.findlines(old[0][1:], oldstart)
771 772 else:
772 773 # Only adding lines with no or fuzzed context, just
773 774 # take the skew in account
774 775 cand = [oldstart]
775 776
776 777 for l in cand:
777 778 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
778 779 self.lines[l : l + len(old)] = new
779 780 self.offset += len(new) - len(old)
780 781 self.skew = l - orig_start
781 782 self.dirty = True
782 783 offset = l - orig_start - fuzzlen
783 784 if fuzzlen:
784 785 msg = _("Hunk #%d succeeded at %d "
785 786 "with fuzz %d "
786 787 "(offset %d lines).\n")
787 788 self.printfile(True)
788 789 self.ui.warn(msg %
789 790 (h.number, l + 1, fuzzlen, offset))
790 791 else:
791 792 msg = _("Hunk #%d succeeded at %d "
792 793 "(offset %d lines).\n")
793 794 self.ui.note(msg % (h.number, l + 1, offset))
794 795 return fuzzlen
795 796 self.printfile(True)
796 797 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
797 798 self.rej.append(horig)
798 799 return -1
799 800
800 801 def close(self):
801 802 if self.dirty:
802 803 self.writelines(self.fname, self.lines, self.mode)
803 804 self.write_rej()
804 805 return len(self.rej)
805 806
806 807 class hunk(object):
807 808 def __init__(self, desc, num, lr, context):
808 809 self.number = num
809 810 self.desc = desc
810 811 self.hunk = [desc]
811 812 self.a = []
812 813 self.b = []
813 814 self.starta = self.lena = None
814 815 self.startb = self.lenb = None
815 816 if lr is not None:
816 817 if context:
817 818 self.read_context_hunk(lr)
818 819 else:
819 820 self.read_unified_hunk(lr)
820 821
821 822 def getnormalized(self):
822 823 """Return a copy with line endings normalized to LF."""
823 824
824 825 def normalize(lines):
825 826 nlines = []
826 827 for line in lines:
827 828 if line.endswith('\r\n'):
828 829 line = line[:-2] + '\n'
829 830 nlines.append(line)
830 831 return nlines
831 832
832 833 # Dummy object, it is rebuilt manually
833 834 nh = hunk(self.desc, self.number, None, None)
834 835 nh.number = self.number
835 836 nh.desc = self.desc
836 837 nh.hunk = self.hunk
837 838 nh.a = normalize(self.a)
838 839 nh.b = normalize(self.b)
839 840 nh.starta = self.starta
840 841 nh.startb = self.startb
841 842 nh.lena = self.lena
842 843 nh.lenb = self.lenb
843 844 return nh
844 845
845 846 def read_unified_hunk(self, lr):
846 847 m = unidesc.match(self.desc)
847 848 if not m:
848 849 raise PatchError(_("bad hunk #%d") % self.number)
849 850 self.starta, self.lena, self.startb, self.lenb = m.groups()
850 851 if self.lena is None:
851 852 self.lena = 1
852 853 else:
853 854 self.lena = int(self.lena)
854 855 if self.lenb is None:
855 856 self.lenb = 1
856 857 else:
857 858 self.lenb = int(self.lenb)
858 859 self.starta = int(self.starta)
859 860 self.startb = int(self.startb)
860 861 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
861 862 self.b)
862 863 # if we hit eof before finishing out the hunk, the last line will
863 864 # be zero length. Lets try to fix it up.
864 865 while len(self.hunk[-1]) == 0:
865 866 del self.hunk[-1]
866 867 del self.a[-1]
867 868 del self.b[-1]
868 869 self.lena -= 1
869 870 self.lenb -= 1
870 871 self._fixnewline(lr)
871 872
872 873 def read_context_hunk(self, lr):
873 874 self.desc = lr.readline()
874 875 m = contextdesc.match(self.desc)
875 876 if not m:
876 877 raise PatchError(_("bad hunk #%d") % self.number)
877 878 self.starta, aend = m.groups()
878 879 self.starta = int(self.starta)
879 880 if aend is None:
880 881 aend = self.starta
881 882 self.lena = int(aend) - self.starta
882 883 if self.starta:
883 884 self.lena += 1
884 885 for x in xrange(self.lena):
885 886 l = lr.readline()
886 887 if l.startswith('---'):
887 888 # lines addition, old block is empty
888 889 lr.push(l)
889 890 break
890 891 s = l[2:]
891 892 if l.startswith('- ') or l.startswith('! '):
892 893 u = '-' + s
893 894 elif l.startswith(' '):
894 895 u = ' ' + s
895 896 else:
896 897 raise PatchError(_("bad hunk #%d old text line %d") %
897 898 (self.number, x))
898 899 self.a.append(u)
899 900 self.hunk.append(u)
900 901
901 902 l = lr.readline()
902 903 if l.startswith('\ '):
903 904 s = self.a[-1][:-1]
904 905 self.a[-1] = s
905 906 self.hunk[-1] = s
906 907 l = lr.readline()
907 908 m = contextdesc.match(l)
908 909 if not m:
909 910 raise PatchError(_("bad hunk #%d") % self.number)
910 911 self.startb, bend = m.groups()
911 912 self.startb = int(self.startb)
912 913 if bend is None:
913 914 bend = self.startb
914 915 self.lenb = int(bend) - self.startb
915 916 if self.startb:
916 917 self.lenb += 1
917 918 hunki = 1
918 919 for x in xrange(self.lenb):
919 920 l = lr.readline()
920 921 if l.startswith('\ '):
921 922 # XXX: the only way to hit this is with an invalid line range.
922 923 # The no-eol marker is not counted in the line range, but I
923 924 # guess there are diff(1) out there which behave differently.
924 925 s = self.b[-1][:-1]
925 926 self.b[-1] = s
926 927 self.hunk[hunki - 1] = s
927 928 continue
928 929 if not l:
929 930 # line deletions, new block is empty and we hit EOF
930 931 lr.push(l)
931 932 break
932 933 s = l[2:]
933 934 if l.startswith('+ ') or l.startswith('! '):
934 935 u = '+' + s
935 936 elif l.startswith(' '):
936 937 u = ' ' + s
937 938 elif len(self.b) == 0:
938 939 # line deletions, new block is empty
939 940 lr.push(l)
940 941 break
941 942 else:
942 943 raise PatchError(_("bad hunk #%d old text line %d") %
943 944 (self.number, x))
944 945 self.b.append(s)
945 946 while True:
946 947 if hunki >= len(self.hunk):
947 948 h = ""
948 949 else:
949 950 h = self.hunk[hunki]
950 951 hunki += 1
951 952 if h == u:
952 953 break
953 954 elif h.startswith('-'):
954 955 continue
955 956 else:
956 957 self.hunk.insert(hunki - 1, u)
957 958 break
958 959
959 960 if not self.a:
960 961 # this happens when lines were only added to the hunk
961 962 for x in self.hunk:
962 963 if x.startswith('-') or x.startswith(' '):
963 964 self.a.append(x)
964 965 if not self.b:
965 966 # this happens when lines were only deleted from the hunk
966 967 for x in self.hunk:
967 968 if x.startswith('+') or x.startswith(' '):
968 969 self.b.append(x[1:])
969 970 # @@ -start,len +start,len @@
970 971 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
971 972 self.startb, self.lenb)
972 973 self.hunk[0] = self.desc
973 974 self._fixnewline(lr)
974 975
975 976 def _fixnewline(self, lr):
976 977 l = lr.readline()
977 978 if l.startswith('\ '):
978 979 diffhelpers.fix_newline(self.hunk, self.a, self.b)
979 980 else:
980 981 lr.push(l)
981 982
982 983 def complete(self):
983 984 return len(self.a) == self.lena and len(self.b) == self.lenb
984 985
985 986 def _fuzzit(self, old, new, fuzz, toponly):
986 987 # this removes context lines from the top and bottom of list 'l'. It
987 988 # checks the hunk to make sure only context lines are removed, and then
988 989 # returns a new shortened list of lines.
989 990 fuzz = min(fuzz, len(old))
990 991 if fuzz:
991 992 top = 0
992 993 bot = 0
993 994 hlen = len(self.hunk)
994 995 for x in xrange(hlen - 1):
995 996 # the hunk starts with the @@ line, so use x+1
996 997 if self.hunk[x + 1][0] == ' ':
997 998 top += 1
998 999 else:
999 1000 break
1000 1001 if not toponly:
1001 1002 for x in xrange(hlen - 1):
1002 1003 if self.hunk[hlen - bot - 1][0] == ' ':
1003 1004 bot += 1
1004 1005 else:
1005 1006 break
1006 1007
1007 1008 bot = min(fuzz, bot)
1008 1009 top = min(fuzz, top)
1009 1010 return old[top:len(old) - bot], new[top:len(new) - bot], top
1010 1011 return old, new, 0
1011 1012
1012 1013 def fuzzit(self, fuzz, toponly):
1013 1014 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1014 1015 oldstart = self.starta + top
1015 1016 newstart = self.startb + top
1016 1017 # zero length hunk ranges already have their start decremented
1017 1018 if self.lena and oldstart > 0:
1018 1019 oldstart -= 1
1019 1020 if self.lenb and newstart > 0:
1020 1021 newstart -= 1
1021 1022 return old, oldstart, new, newstart
1022 1023
1023 1024 class binhunk(object):
1024 1025 'A binary patch file.'
1025 1026 def __init__(self, lr, fname):
1026 1027 self.text = None
1027 1028 self.delta = False
1028 1029 self.hunk = ['GIT binary patch\n']
1029 1030 self._fname = fname
1030 1031 self._read(lr)
1031 1032
1032 1033 def complete(self):
1033 1034 return self.text is not None
1034 1035
1035 1036 def new(self, lines):
1036 1037 if self.delta:
1037 1038 return [applybindelta(self.text, ''.join(lines))]
1038 1039 return [self.text]
1039 1040
1040 1041 def _read(self, lr):
1041 1042 def getline(lr, hunk):
1042 1043 l = lr.readline()
1043 1044 hunk.append(l)
1044 1045 return l.rstrip('\r\n')
1045 1046
1046 1047 size = 0
1047 1048 while True:
1048 1049 line = getline(lr, self.hunk)
1049 1050 if not line:
1050 1051 raise PatchError(_('could not extract "%s" binary data')
1051 1052 % self._fname)
1052 1053 if line.startswith('literal '):
1053 1054 size = int(line[8:].rstrip())
1054 1055 break
1055 1056 if line.startswith('delta '):
1056 1057 size = int(line[6:].rstrip())
1057 1058 self.delta = True
1058 1059 break
1059 1060 dec = []
1060 1061 line = getline(lr, self.hunk)
1061 1062 while len(line) > 1:
1062 1063 l = line[0]
1063 1064 if l <= 'Z' and l >= 'A':
1064 1065 l = ord(l) - ord('A') + 1
1065 1066 else:
1066 1067 l = ord(l) - ord('a') + 27
1067 1068 try:
1068 1069 dec.append(base85.b85decode(line[1:])[:l])
1069 1070 except ValueError, e:
1070 1071 raise PatchError(_('could not decode "%s" binary patch: %s')
1071 1072 % (self._fname, str(e)))
1072 1073 line = getline(lr, self.hunk)
1073 1074 text = zlib.decompress(''.join(dec))
1074 1075 if len(text) != size:
1075 1076 raise PatchError(_('"%s" length is %d bytes, should be %d')
1076 1077 % (self._fname, len(text), size))
1077 1078 self.text = text
1078 1079
1079 1080 def parsefilename(str):
1080 1081 # --- filename \t|space stuff
1081 1082 s = str[4:].rstrip('\r\n')
1082 1083 i = s.find('\t')
1083 1084 if i < 0:
1084 1085 i = s.find(' ')
1085 1086 if i < 0:
1086 1087 return s
1087 1088 return s[:i]
1088 1089
1089 1090 def pathstrip(path, strip):
1090 1091 pathlen = len(path)
1091 1092 i = 0
1092 1093 if strip == 0:
1093 1094 return '', path.rstrip()
1094 1095 count = strip
1095 1096 while count > 0:
1096 1097 i = path.find('/', i)
1097 1098 if i == -1:
1098 1099 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1099 1100 (count, strip, path))
1100 1101 i += 1
1101 1102 # consume '//' in the path
1102 1103 while i < pathlen - 1 and path[i] == '/':
1103 1104 i += 1
1104 1105 count -= 1
1105 1106 return path[:i].lstrip(), path[i:].rstrip()
1106 1107
1107 1108 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip):
1108 1109 nulla = afile_orig == "/dev/null"
1109 1110 nullb = bfile_orig == "/dev/null"
1110 1111 create = nulla and hunk.starta == 0 and hunk.lena == 0
1111 1112 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1112 1113 abase, afile = pathstrip(afile_orig, strip)
1113 1114 gooda = not nulla and backend.exists(afile)
1114 1115 bbase, bfile = pathstrip(bfile_orig, strip)
1115 1116 if afile == bfile:
1116 1117 goodb = gooda
1117 1118 else:
1118 1119 goodb = not nullb and backend.exists(bfile)
1119 1120 missing = not goodb and not gooda and not create
1120 1121
1121 1122 # some diff programs apparently produce patches where the afile is
1122 1123 # not /dev/null, but afile starts with bfile
1123 1124 abasedir = afile[:afile.rfind('/') + 1]
1124 1125 bbasedir = bfile[:bfile.rfind('/') + 1]
1125 1126 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1126 1127 and hunk.starta == 0 and hunk.lena == 0):
1127 1128 create = True
1128 1129 missing = False
1129 1130
1130 1131 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1131 1132 # diff is between a file and its backup. In this case, the original
1132 1133 # file should be patched (see original mpatch code).
1133 1134 isbackup = (abase == bbase and bfile.startswith(afile))
1134 1135 fname = None
1135 1136 if not missing:
1136 1137 if gooda and goodb:
1137 1138 fname = isbackup and afile or bfile
1138 1139 elif gooda:
1139 1140 fname = afile
1140 1141
1141 1142 if not fname:
1142 1143 if not nullb:
1143 1144 fname = isbackup and afile or bfile
1144 1145 elif not nulla:
1145 1146 fname = afile
1146 1147 else:
1147 1148 raise PatchError(_("undefined source and destination files"))
1148 1149
1149 1150 gp = patchmeta(fname)
1150 1151 if create:
1151 1152 gp.op = 'ADD'
1152 1153 elif remove:
1153 1154 gp.op = 'DELETE'
1154 1155 return gp
1155 1156
1156 1157 def scangitpatch(lr, firstline):
1157 1158 """
1158 1159 Git patches can emit:
1159 1160 - rename a to b
1160 1161 - change b
1161 1162 - copy a to c
1162 1163 - change c
1163 1164
1164 1165 We cannot apply this sequence as-is, the renamed 'a' could not be
1165 1166 found for it would have been renamed already. And we cannot copy
1166 1167 from 'b' instead because 'b' would have been changed already. So
1167 1168 we scan the git patch for copy and rename commands so we can
1168 1169 perform the copies ahead of time.
1169 1170 """
1170 1171 pos = 0
1171 1172 try:
1172 1173 pos = lr.fp.tell()
1173 1174 fp = lr.fp
1174 1175 except IOError:
1175 1176 fp = cStringIO.StringIO(lr.fp.read())
1176 1177 gitlr = linereader(fp)
1177 1178 gitlr.push(firstline)
1178 1179 gitpatches = readgitpatch(gitlr)
1179 1180 fp.seek(pos)
1180 1181 return gitpatches
1181 1182
1182 1183 def iterhunks(fp):
1183 1184 """Read a patch and yield the following events:
1184 1185 - ("file", afile, bfile, firsthunk): select a new target file.
1185 1186 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1186 1187 "file" event.
1187 1188 - ("git", gitchanges): current diff is in git format, gitchanges
1188 1189 maps filenames to gitpatch records. Unique event.
1189 1190 """
1190 1191 afile = ""
1191 1192 bfile = ""
1192 1193 state = None
1193 1194 hunknum = 0
1194 1195 emitfile = newfile = False
1195 1196 gitpatches = None
1196 1197
1197 1198 # our states
1198 1199 BFILE = 1
1199 1200 context = None
1200 1201 lr = linereader(fp)
1201 1202
1202 1203 while True:
1203 1204 x = lr.readline()
1204 1205 if not x:
1205 1206 break
1206 1207 if state == BFILE and (
1207 1208 (not context and x[0] == '@')
1208 1209 or (context is not False and x.startswith('***************'))
1209 1210 or x.startswith('GIT binary patch')):
1210 1211 gp = None
1211 1212 if (gitpatches and
1212 1213 gitpatches[-1].ispatching(afile, bfile)):
1213 1214 gp = gitpatches.pop()
1214 1215 if x.startswith('GIT binary patch'):
1215 1216 h = binhunk(lr, gp.path)
1216 1217 else:
1217 1218 if context is None and x.startswith('***************'):
1218 1219 context = True
1219 1220 h = hunk(x, hunknum + 1, lr, context)
1220 1221 hunknum += 1
1221 1222 if emitfile:
1222 1223 emitfile = False
1223 1224 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1224 1225 yield 'hunk', h
1225 1226 elif x.startswith('diff --git a/'):
1226 1227 m = gitre.match(x.rstrip(' \r\n'))
1227 1228 if not m:
1228 1229 continue
1229 1230 if gitpatches is None:
1230 1231 # scan whole input for git metadata
1231 1232 gitpatches = scangitpatch(lr, x)
1232 1233 yield 'git', [g.copy() for g in gitpatches
1233 1234 if g.op in ('COPY', 'RENAME')]
1234 1235 gitpatches.reverse()
1235 1236 afile = 'a/' + m.group(1)
1236 1237 bfile = 'b/' + m.group(2)
1237 1238 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1238 1239 gp = gitpatches.pop()
1239 1240 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1240 1241 if not gitpatches:
1241 1242 raise PatchError(_('failed to synchronize metadata for "%s"')
1242 1243 % afile[2:])
1243 1244 gp = gitpatches[-1]
1244 1245 newfile = True
1245 1246 elif x.startswith('---'):
1246 1247 # check for a unified diff
1247 1248 l2 = lr.readline()
1248 1249 if not l2.startswith('+++'):
1249 1250 lr.push(l2)
1250 1251 continue
1251 1252 newfile = True
1252 1253 context = False
1253 1254 afile = parsefilename(x)
1254 1255 bfile = parsefilename(l2)
1255 1256 elif x.startswith('***'):
1256 1257 # check for a context diff
1257 1258 l2 = lr.readline()
1258 1259 if not l2.startswith('---'):
1259 1260 lr.push(l2)
1260 1261 continue
1261 1262 l3 = lr.readline()
1262 1263 lr.push(l3)
1263 1264 if not l3.startswith("***************"):
1264 1265 lr.push(l2)
1265 1266 continue
1266 1267 newfile = True
1267 1268 context = True
1268 1269 afile = parsefilename(x)
1269 1270 bfile = parsefilename(l2)
1270 1271
1271 1272 if newfile:
1272 1273 newfile = False
1273 1274 emitfile = True
1274 1275 state = BFILE
1275 1276 hunknum = 0
1276 1277
1277 1278 while gitpatches:
1278 1279 gp = gitpatches.pop()
1279 1280 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1280 1281
1281 1282 def applybindelta(binchunk, data):
1282 1283 """Apply a binary delta hunk
1283 1284 The algorithm used is the algorithm from git's patch-delta.c
1284 1285 """
1285 1286 def deltahead(binchunk):
1286 1287 i = 0
1287 1288 for c in binchunk:
1288 1289 i += 1
1289 1290 if not (ord(c) & 0x80):
1290 1291 return i
1291 1292 return i
1292 1293 out = ""
1293 1294 s = deltahead(binchunk)
1294 1295 binchunk = binchunk[s:]
1295 1296 s = deltahead(binchunk)
1296 1297 binchunk = binchunk[s:]
1297 1298 i = 0
1298 1299 while i < len(binchunk):
1299 1300 cmd = ord(binchunk[i])
1300 1301 i += 1
1301 1302 if (cmd & 0x80):
1302 1303 offset = 0
1303 1304 size = 0
1304 1305 if (cmd & 0x01):
1305 1306 offset = ord(binchunk[i])
1306 1307 i += 1
1307 1308 if (cmd & 0x02):
1308 1309 offset |= ord(binchunk[i]) << 8
1309 1310 i += 1
1310 1311 if (cmd & 0x04):
1311 1312 offset |= ord(binchunk[i]) << 16
1312 1313 i += 1
1313 1314 if (cmd & 0x08):
1314 1315 offset |= ord(binchunk[i]) << 24
1315 1316 i += 1
1316 1317 if (cmd & 0x10):
1317 1318 size = ord(binchunk[i])
1318 1319 i += 1
1319 1320 if (cmd & 0x20):
1320 1321 size |= ord(binchunk[i]) << 8
1321 1322 i += 1
1322 1323 if (cmd & 0x40):
1323 1324 size |= ord(binchunk[i]) << 16
1324 1325 i += 1
1325 1326 if size == 0:
1326 1327 size = 0x10000
1327 1328 offset_end = offset + size
1328 1329 out += data[offset:offset_end]
1329 1330 elif cmd != 0:
1330 1331 offset_end = i + cmd
1331 1332 out += binchunk[i:offset_end]
1332 1333 i += cmd
1333 1334 else:
1334 1335 raise PatchError(_('unexpected delta opcode 0'))
1335 1336 return out
1336 1337
1337 1338 def applydiff(ui, fp, backend, store, strip=1, eolmode='strict'):
1338 1339 """Reads a patch from fp and tries to apply it.
1339 1340
1340 1341 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1341 1342 there was any fuzz.
1342 1343
1343 1344 If 'eolmode' is 'strict', the patch content and patched file are
1344 1345 read in binary mode. Otherwise, line endings are ignored when
1345 1346 patching then normalized according to 'eolmode'.
1346 1347 """
1347 1348 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1348 1349 eolmode=eolmode)
1349 1350
1350 1351 def _applydiff(ui, fp, patcher, backend, store, strip=1,
1351 1352 eolmode='strict'):
1352 1353
1353 1354 def pstrip(p):
1354 1355 return pathstrip(p, strip - 1)[1]
1355 1356
1356 1357 rejects = 0
1357 1358 err = 0
1358 1359 current_file = None
1359 1360
1360 1361 for state, values in iterhunks(fp):
1361 1362 if state == 'hunk':
1362 1363 if not current_file:
1363 1364 continue
1364 1365 ret = current_file.apply(values)
1365 1366 if ret > 0:
1366 1367 err = 1
1367 1368 elif state == 'file':
1368 1369 if current_file:
1369 1370 rejects += current_file.close()
1370 1371 current_file = None
1371 1372 afile, bfile, first_hunk, gp = values
1372 1373 if gp:
1373 1374 gp.path = pstrip(gp.path)
1374 1375 if gp.oldpath:
1375 1376 gp.oldpath = pstrip(gp.oldpath)
1376 1377 else:
1377 1378 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1378 1379 if gp.op == 'RENAME':
1379 1380 backend.unlink(gp.oldpath)
1380 1381 if not first_hunk:
1381 1382 if gp.op == 'DELETE':
1382 1383 backend.unlink(gp.path)
1383 1384 continue
1384 1385 data, mode = None, None
1385 1386 if gp.op in ('RENAME', 'COPY'):
1386 1387 data, mode = store.getfile(gp.oldpath)[:2]
1387 1388 # FIXME: failing getfile has never been handled here
1388 1389 assert data is not None
1389 1390 if gp.mode:
1390 1391 mode = gp.mode
1391 1392 if gp.op == 'ADD':
1392 1393 # Added files without content have no hunk and
1393 1394 # must be created
1394 1395 data = ''
1395 1396 if data or mode:
1396 1397 if (gp.op in ('ADD', 'RENAME', 'COPY')
1397 1398 and backend.exists(gp.path)):
1398 1399 raise PatchError(_("cannot create %s: destination "
1399 1400 "already exists") % gp.path)
1400 1401 backend.setfile(gp.path, data, mode, gp.oldpath)
1401 1402 continue
1402 1403 try:
1403 1404 current_file = patcher(ui, gp, backend, store,
1404 1405 eolmode=eolmode)
1405 1406 except PatchError, inst:
1406 1407 ui.warn(str(inst) + '\n')
1407 1408 current_file = None
1408 1409 rejects += 1
1409 1410 continue
1410 1411 elif state == 'git':
1411 1412 for gp in values:
1412 1413 path = pstrip(gp.oldpath)
1413 1414 data, mode = backend.getfile(path)
1414 1415 if data is None:
1415 1416 # The error ignored here will trigger a getfile()
1416 1417 # error in a place more appropriate for error
1417 1418 # handling, and will not interrupt the patching
1418 1419 # process.
1419 1420 pass
1420 1421 else:
1421 1422 store.setfile(path, data, mode)
1422 1423 else:
1423 1424 raise util.Abort(_('unsupported parser state: %s') % state)
1424 1425
1425 1426 if current_file:
1426 1427 rejects += current_file.close()
1427 1428
1428 1429 if rejects:
1429 1430 return -1
1430 1431 return err
1431 1432
1432 1433 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1433 1434 similarity):
1434 1435 """use <patcher> to apply <patchname> to the working directory.
1435 1436 returns whether patch was applied with fuzz factor."""
1436 1437
1437 1438 fuzz = False
1438 1439 args = []
1439 1440 cwd = repo.root
1440 1441 if cwd:
1441 1442 args.append('-d %s' % util.shellquote(cwd))
1442 1443 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1443 1444 util.shellquote(patchname)))
1444 1445 try:
1445 1446 for line in fp:
1446 1447 line = line.rstrip()
1447 1448 ui.note(line + '\n')
1448 1449 if line.startswith('patching file '):
1449 1450 pf = util.parsepatchoutput(line)
1450 1451 printed_file = False
1451 1452 files.add(pf)
1452 1453 elif line.find('with fuzz') >= 0:
1453 1454 fuzz = True
1454 1455 if not printed_file:
1455 1456 ui.warn(pf + '\n')
1456 1457 printed_file = True
1457 1458 ui.warn(line + '\n')
1458 1459 elif line.find('saving rejects to file') >= 0:
1459 1460 ui.warn(line + '\n')
1460 1461 elif line.find('FAILED') >= 0:
1461 1462 if not printed_file:
1462 1463 ui.warn(pf + '\n')
1463 1464 printed_file = True
1464 1465 ui.warn(line + '\n')
1465 1466 finally:
1466 1467 if files:
1467 1468 scmutil.marktouched(repo, files, similarity)
1468 1469 code = fp.close()
1469 1470 if code:
1470 1471 raise PatchError(_("patch command failed: %s") %
1471 1472 util.explainexit(code)[0])
1472 1473 return fuzz
1473 1474
1474 1475 def patchbackend(ui, backend, patchobj, strip, files=None, eolmode='strict'):
1475 1476 if files is None:
1476 1477 files = set()
1477 1478 if eolmode is None:
1478 1479 eolmode = ui.config('patch', 'eol', 'strict')
1479 1480 if eolmode.lower() not in eolmodes:
1480 1481 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1481 1482 eolmode = eolmode.lower()
1482 1483
1483 1484 store = filestore()
1484 1485 try:
1485 1486 fp = open(patchobj, 'rb')
1486 1487 except TypeError:
1487 1488 fp = patchobj
1488 1489 try:
1489 1490 ret = applydiff(ui, fp, backend, store, strip=strip,
1490 1491 eolmode=eolmode)
1491 1492 finally:
1492 1493 if fp != patchobj:
1493 1494 fp.close()
1494 1495 files.update(backend.close())
1495 1496 store.close()
1496 1497 if ret < 0:
1497 1498 raise PatchError(_('patch failed to apply'))
1498 1499 return ret > 0
1499 1500
1500 1501 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1501 1502 similarity=0):
1502 1503 """use builtin patch to apply <patchobj> to the working directory.
1503 1504 returns whether patch was applied with fuzz factor."""
1504 1505 backend = workingbackend(ui, repo, similarity)
1505 1506 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1506 1507
1507 1508 def patchrepo(ui, repo, ctx, store, patchobj, strip, files=None,
1508 1509 eolmode='strict'):
1509 1510 backend = repobackend(ui, repo, ctx, store)
1510 1511 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1511 1512
1512 1513 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1513 1514 similarity=0):
1514 1515 """Apply <patchname> to the working directory.
1515 1516
1516 1517 'eolmode' specifies how end of lines should be handled. It can be:
1517 1518 - 'strict': inputs are read in binary mode, EOLs are preserved
1518 1519 - 'crlf': EOLs are ignored when patching and reset to CRLF
1519 1520 - 'lf': EOLs are ignored when patching and reset to LF
1520 1521 - None: get it from user settings, default to 'strict'
1521 1522 'eolmode' is ignored when using an external patcher program.
1522 1523
1523 1524 Returns whether patch was applied with fuzz factor.
1524 1525 """
1525 1526 patcher = ui.config('ui', 'patch')
1526 1527 if files is None:
1527 1528 files = set()
1528 1529 if patcher:
1529 1530 return _externalpatch(ui, repo, patcher, patchname, strip,
1530 1531 files, similarity)
1531 1532 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1532 1533 similarity)
1533 1534
1534 1535 def changedfiles(ui, repo, patchpath, strip=1):
1535 1536 backend = fsbackend(ui, repo.root)
1536 1537 fp = open(patchpath, 'rb')
1537 1538 try:
1538 1539 changed = set()
1539 1540 for state, values in iterhunks(fp):
1540 1541 if state == 'file':
1541 1542 afile, bfile, first_hunk, gp = values
1542 1543 if gp:
1543 1544 gp.path = pathstrip(gp.path, strip - 1)[1]
1544 1545 if gp.oldpath:
1545 1546 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1546 1547 else:
1547 1548 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1548 1549 changed.add(gp.path)
1549 1550 if gp.op == 'RENAME':
1550 1551 changed.add(gp.oldpath)
1551 1552 elif state not in ('hunk', 'git'):
1552 1553 raise util.Abort(_('unsupported parser state: %s') % state)
1553 1554 return changed
1554 1555 finally:
1555 1556 fp.close()
1556 1557
1557 1558 class GitDiffRequired(Exception):
1558 1559 pass
1559 1560
1560 1561 def diffopts(ui, opts=None, untrusted=False, section='diff'):
1561 1562 def get(key, name=None, getter=ui.configbool):
1562 1563 return ((opts and opts.get(key)) or
1563 1564 getter(section, name or key, None, untrusted=untrusted))
1564 1565 return mdiff.diffopts(
1565 1566 text=opts and opts.get('text'),
1566 1567 git=get('git'),
1567 1568 nodates=get('nodates'),
1568 1569 nobinary=get('nobinary'),
1569 1570 showfunc=get('show_function', 'showfunc'),
1570 1571 ignorews=get('ignore_all_space', 'ignorews'),
1571 1572 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1572 1573 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1573 1574 context=get('unified', getter=ui.config))
1574 1575
1575 1576 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1576 1577 losedatafn=None, prefix=''):
1577 1578 '''yields diff of changes to files between two nodes, or node and
1578 1579 working directory.
1579 1580
1580 1581 if node1 is None, use first dirstate parent instead.
1581 1582 if node2 is None, compare node1 with working directory.
1582 1583
1583 1584 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1584 1585 every time some change cannot be represented with the current
1585 1586 patch format. Return False to upgrade to git patch format, True to
1586 1587 accept the loss or raise an exception to abort the diff. It is
1587 1588 called with the name of current file being diffed as 'fn'. If set
1588 1589 to None, patches will always be upgraded to git format when
1589 1590 necessary.
1590 1591
1591 1592 prefix is a filename prefix that is prepended to all filenames on
1592 1593 display (used for subrepos).
1593 1594 '''
1594 1595
1595 1596 if opts is None:
1596 1597 opts = mdiff.defaultopts
1597 1598
1598 1599 if not node1 and not node2:
1599 1600 node1 = repo.dirstate.p1()
1600 1601
1601 1602 def lrugetfilectx():
1602 1603 cache = {}
1603 1604 order = util.deque()
1604 1605 def getfilectx(f, ctx):
1605 1606 fctx = ctx.filectx(f, filelog=cache.get(f))
1606 1607 if f not in cache:
1607 1608 if len(cache) > 20:
1608 1609 del cache[order.popleft()]
1609 1610 cache[f] = fctx.filelog()
1610 1611 else:
1611 1612 order.remove(f)
1612 1613 order.append(f)
1613 1614 return fctx
1614 1615 return getfilectx
1615 1616 getfilectx = lrugetfilectx()
1616 1617
1617 1618 ctx1 = repo[node1]
1618 1619 ctx2 = repo[node2]
1619 1620
1620 1621 if not changes:
1621 1622 changes = repo.status(ctx1, ctx2, match=match)
1622 1623 modified, added, removed = changes[:3]
1623 1624
1624 1625 if not modified and not added and not removed:
1625 1626 return []
1626 1627
1627 1628 revs = None
1628 1629 hexfunc = repo.ui.debugflag and hex or short
1629 1630 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
1630 1631
1631 1632 copy = {}
1632 1633 if opts.git or opts.upgrade:
1633 1634 copy = copies.pathcopies(ctx1, ctx2)
1634 1635
1635 1636 def difffn(opts, losedata):
1636 1637 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1637 1638 copy, getfilectx, opts, losedata, prefix)
1638 1639 if opts.upgrade and not opts.git:
1639 1640 try:
1640 1641 def losedata(fn):
1641 1642 if not losedatafn or not losedatafn(fn=fn):
1642 1643 raise GitDiffRequired
1643 1644 # Buffer the whole output until we are sure it can be generated
1644 1645 return list(difffn(opts.copy(git=False), losedata))
1645 1646 except GitDiffRequired:
1646 1647 return difffn(opts.copy(git=True), None)
1647 1648 else:
1648 1649 return difffn(opts, None)
1649 1650
1650 1651 def difflabel(func, *args, **kw):
1651 1652 '''yields 2-tuples of (output, label) based on the output of func()'''
1652 1653 headprefixes = [('diff', 'diff.diffline'),
1653 1654 ('copy', 'diff.extended'),
1654 1655 ('rename', 'diff.extended'),
1655 1656 ('old', 'diff.extended'),
1656 1657 ('new', 'diff.extended'),
1657 1658 ('deleted', 'diff.extended'),
1658 1659 ('---', 'diff.file_a'),
1659 1660 ('+++', 'diff.file_b')]
1660 1661 textprefixes = [('@', 'diff.hunk'),
1661 1662 ('-', 'diff.deleted'),
1662 1663 ('+', 'diff.inserted')]
1663 1664 head = False
1664 1665 for chunk in func(*args, **kw):
1665 1666 lines = chunk.split('\n')
1666 1667 for i, line in enumerate(lines):
1667 1668 if i != 0:
1668 1669 yield ('\n', '')
1669 1670 if head:
1670 1671 if line.startswith('@'):
1671 1672 head = False
1672 1673 else:
1673 1674 if line and line[0] not in ' +-@\\':
1674 1675 head = True
1675 1676 stripline = line
1677 diffline = False
1676 1678 if not head and line and line[0] in '+-':
1677 # highlight trailing whitespace, but only in changed lines
1679 # highlight tabs and trailing whitespace, but only in
1680 # changed lines
1678 1681 stripline = line.rstrip()
1682 diffline = True
1683
1679 1684 prefixes = textprefixes
1680 1685 if head:
1681 1686 prefixes = headprefixes
1682 1687 for prefix, label in prefixes:
1683 1688 if stripline.startswith(prefix):
1684 yield (stripline, label)
1689 if diffline:
1690 for token in tabsplitter.findall(stripline):
1691 if '\t' == token[0]:
1692 yield (token, 'diff.tab')
1693 else:
1694 yield (token, label)
1695 else:
1696 yield (stripline, label)
1685 1697 break
1686 1698 else:
1687 1699 yield (line, '')
1688 1700 if line != stripline:
1689 1701 yield (line[len(stripline):], 'diff.trailingwhitespace')
1690 1702
1691 1703 def diffui(*args, **kw):
1692 1704 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1693 1705 return difflabel(diff, *args, **kw)
1694 1706
1695 1707 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1696 1708 copy, getfilectx, opts, losedatafn, prefix):
1697 1709
1698 1710 def join(f):
1699 1711 return posixpath.join(prefix, f)
1700 1712
1701 1713 def addmodehdr(header, omode, nmode):
1702 1714 if omode != nmode:
1703 1715 header.append('old mode %s\n' % omode)
1704 1716 header.append('new mode %s\n' % nmode)
1705 1717
1706 1718 def addindexmeta(meta, revs):
1707 1719 if opts.git:
1708 1720 i = len(revs)
1709 1721 if i==2:
1710 1722 meta.append('index %s..%s\n' % tuple(revs))
1711 1723 elif i==3:
1712 1724 meta.append('index %s,%s..%s\n' % tuple(revs))
1713 1725
1714 1726 def gitindex(text):
1715 1727 if not text:
1716 1728 text = ""
1717 1729 l = len(text)
1718 1730 s = util.sha1('blob %d\0' % l)
1719 1731 s.update(text)
1720 1732 return s.hexdigest()
1721 1733
1722 1734 def diffline(a, b, revs):
1723 1735 if opts.git:
1724 1736 line = 'diff --git a/%s b/%s\n' % (a, b)
1725 1737 elif not repo.ui.quiet:
1726 1738 if revs:
1727 1739 revinfo = ' '.join(["-r %s" % rev for rev in revs])
1728 1740 line = 'diff %s %s\n' % (revinfo, a)
1729 1741 else:
1730 1742 line = 'diff %s\n' % a
1731 1743 else:
1732 1744 line = ''
1733 1745 return line
1734 1746
1735 1747 date1 = util.datestr(ctx1.date())
1736 1748 man1 = ctx1.manifest()
1737 1749
1738 1750 gone = set()
1739 1751 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1740 1752
1741 1753 copyto = dict([(v, k) for k, v in copy.items()])
1742 1754
1743 1755 if opts.git:
1744 1756 revs = None
1745 1757
1746 1758 for f in sorted(modified + added + removed):
1747 1759 to = None
1748 1760 tn = None
1749 1761 dodiff = True
1750 1762 header = []
1751 1763 if f in man1:
1752 1764 to = getfilectx(f, ctx1).data()
1753 1765 if f not in removed:
1754 1766 tn = getfilectx(f, ctx2).data()
1755 1767 a, b = f, f
1756 1768 if opts.git or losedatafn:
1757 1769 if f in added or (f in modified and to is None):
1758 1770 mode = gitmode[ctx2.flags(f)]
1759 1771 if f in copy or f in copyto:
1760 1772 if opts.git:
1761 1773 if f in copy:
1762 1774 a = copy[f]
1763 1775 else:
1764 1776 a = copyto[f]
1765 1777 omode = gitmode[man1.flags(a)]
1766 1778 addmodehdr(header, omode, mode)
1767 1779 if a in removed and a not in gone:
1768 1780 op = 'rename'
1769 1781 gone.add(a)
1770 1782 else:
1771 1783 op = 'copy'
1772 1784 header.append('%s from %s\n' % (op, join(a)))
1773 1785 header.append('%s to %s\n' % (op, join(f)))
1774 1786 to = getfilectx(a, ctx1).data()
1775 1787 else:
1776 1788 losedatafn(f)
1777 1789 else:
1778 1790 if opts.git:
1779 1791 header.append('new file mode %s\n' % mode)
1780 1792 elif ctx2.flags(f):
1781 1793 losedatafn(f)
1782 1794 # In theory, if tn was copied or renamed we should check
1783 1795 # if the source is binary too but the copy record already
1784 1796 # forces git mode.
1785 1797 if util.binary(tn):
1786 1798 if opts.git:
1787 1799 dodiff = 'binary'
1788 1800 else:
1789 1801 losedatafn(f)
1790 1802 if not opts.git and not tn:
1791 1803 # regular diffs cannot represent new empty file
1792 1804 losedatafn(f)
1793 1805 elif f in removed or (f in modified and tn is None):
1794 1806 if opts.git:
1795 1807 # have we already reported a copy above?
1796 1808 if ((f in copy and copy[f] in added
1797 1809 and copyto[copy[f]] == f) or
1798 1810 (f in copyto and copyto[f] in added
1799 1811 and copy[copyto[f]] == f)):
1800 1812 dodiff = False
1801 1813 else:
1802 1814 header.append('deleted file mode %s\n' %
1803 1815 gitmode[man1.flags(f)])
1804 1816 if util.binary(to):
1805 1817 dodiff = 'binary'
1806 1818 elif not to or util.binary(to):
1807 1819 # regular diffs cannot represent empty file deletion
1808 1820 losedatafn(f)
1809 1821 else:
1810 1822 oflag = man1.flags(f)
1811 1823 nflag = ctx2.flags(f)
1812 1824 binary = util.binary(to) or util.binary(tn)
1813 1825 if opts.git:
1814 1826 addmodehdr(header, gitmode[oflag], gitmode[nflag])
1815 1827 if binary:
1816 1828 dodiff = 'binary'
1817 1829 elif binary or nflag != oflag:
1818 1830 losedatafn(f)
1819 1831
1820 1832 if dodiff:
1821 1833 if opts.git or revs:
1822 1834 header.insert(0, diffline(join(a), join(b), revs))
1823 1835 if dodiff == 'binary' and not opts.nobinary:
1824 1836 text = mdiff.b85diff(to, tn)
1825 1837 if text:
1826 1838 addindexmeta(header, [gitindex(to), gitindex(tn)])
1827 1839 else:
1828 1840 text = mdiff.unidiff(to, date1,
1829 1841 # ctx2 date may be dynamic
1830 1842 tn, util.datestr(ctx2.date()),
1831 1843 join(a), join(b), opts=opts)
1832 1844 if header and (text or len(header) > 1):
1833 1845 yield ''.join(header)
1834 1846 if text:
1835 1847 yield text
1836 1848
1837 1849 def diffstatsum(stats):
1838 1850 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
1839 1851 for f, a, r, b in stats:
1840 1852 maxfile = max(maxfile, encoding.colwidth(f))
1841 1853 maxtotal = max(maxtotal, a + r)
1842 1854 addtotal += a
1843 1855 removetotal += r
1844 1856 binary = binary or b
1845 1857
1846 1858 return maxfile, maxtotal, addtotal, removetotal, binary
1847 1859
1848 1860 def diffstatdata(lines):
1849 1861 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1850 1862
1851 1863 results = []
1852 1864 filename, adds, removes, isbinary = None, 0, 0, False
1853 1865
1854 1866 def addresult():
1855 1867 if filename:
1856 1868 results.append((filename, adds, removes, isbinary))
1857 1869
1858 1870 for line in lines:
1859 1871 if line.startswith('diff'):
1860 1872 addresult()
1861 1873 # set numbers to 0 anyway when starting new file
1862 1874 adds, removes, isbinary = 0, 0, False
1863 1875 if line.startswith('diff --git a/'):
1864 1876 filename = gitre.search(line).group(2)
1865 1877 elif line.startswith('diff -r'):
1866 1878 # format: "diff -r ... -r ... filename"
1867 1879 filename = diffre.search(line).group(1)
1868 1880 elif line.startswith('+') and not line.startswith('+++ '):
1869 1881 adds += 1
1870 1882 elif line.startswith('-') and not line.startswith('--- '):
1871 1883 removes += 1
1872 1884 elif (line.startswith('GIT binary patch') or
1873 1885 line.startswith('Binary file')):
1874 1886 isbinary = True
1875 1887 addresult()
1876 1888 return results
1877 1889
1878 1890 def diffstat(lines, width=80, git=False):
1879 1891 output = []
1880 1892 stats = diffstatdata(lines)
1881 1893 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
1882 1894
1883 1895 countwidth = len(str(maxtotal))
1884 1896 if hasbinary and countwidth < 3:
1885 1897 countwidth = 3
1886 1898 graphwidth = width - countwidth - maxname - 6
1887 1899 if graphwidth < 10:
1888 1900 graphwidth = 10
1889 1901
1890 1902 def scale(i):
1891 1903 if maxtotal <= graphwidth:
1892 1904 return i
1893 1905 # If diffstat runs out of room it doesn't print anything,
1894 1906 # which isn't very useful, so always print at least one + or -
1895 1907 # if there were at least some changes.
1896 1908 return max(i * graphwidth // maxtotal, int(bool(i)))
1897 1909
1898 1910 for filename, adds, removes, isbinary in stats:
1899 1911 if isbinary:
1900 1912 count = 'Bin'
1901 1913 else:
1902 1914 count = adds + removes
1903 1915 pluses = '+' * scale(adds)
1904 1916 minuses = '-' * scale(removes)
1905 1917 output.append(' %s%s | %*s %s%s\n' %
1906 1918 (filename, ' ' * (maxname - encoding.colwidth(filename)),
1907 1919 countwidth, count, pluses, minuses))
1908 1920
1909 1921 if stats:
1910 1922 output.append(_(' %d files changed, %d insertions(+), '
1911 1923 '%d deletions(-)\n')
1912 1924 % (len(stats), totaladds, totalremoves))
1913 1925
1914 1926 return ''.join(output)
1915 1927
1916 1928 def diffstatui(*args, **kw):
1917 1929 '''like diffstat(), but yields 2-tuples of (output, label) for
1918 1930 ui.write()
1919 1931 '''
1920 1932
1921 1933 for line in diffstat(*args, **kw).splitlines():
1922 1934 if line and line[-1] in '+-':
1923 1935 name, graph = line.rsplit(' ', 1)
1924 1936 yield (name + ' ', '')
1925 1937 m = re.search(r'\++', graph)
1926 1938 if m:
1927 1939 yield (m.group(0), 'diffstat.inserted')
1928 1940 m = re.search(r'-+', graph)
1929 1941 if m:
1930 1942 yield (m.group(0), 'diffstat.deleted')
1931 1943 else:
1932 1944 yield (line, '')
1933 1945 yield ('\n', '')
@@ -1,162 +1,202 b''
1 1 Setup
2 2
3 3 $ echo "[color]" >> $HGRCPATH
4 4 $ echo "mode = ansi" >> $HGRCPATH
5 5 $ echo "[extensions]" >> $HGRCPATH
6 6 $ echo "color=" >> $HGRCPATH
7 7 $ hg init repo
8 8 $ cd repo
9 9 $ cat > a <<EOF
10 10 > c
11 11 > c
12 12 > a
13 13 > a
14 14 > b
15 15 > a
16 16 > a
17 17 > c
18 18 > c
19 19 > EOF
20 20 $ hg ci -Am adda
21 21 adding a
22 22 $ cat > a <<EOF
23 23 > c
24 24 > c
25 25 > a
26 26 > a
27 27 > dd
28 28 > a
29 29 > a
30 30 > c
31 31 > c
32 32 > EOF
33 33
34 34 default context
35 35
36 36 $ hg diff --nodates --color=always
37 37 \x1b[0;1mdiff -r cf9f4ba66af2 a\x1b[0m (esc)
38 38 \x1b[0;31;1m--- a/a\x1b[0m (esc)
39 39 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
40 40 \x1b[0;35m@@ -2,7 +2,7 @@\x1b[0m (esc)
41 41 c
42 42 a
43 43 a
44 44 \x1b[0;31m-b\x1b[0m (esc)
45 45 \x1b[0;32m+dd\x1b[0m (esc)
46 46 a
47 47 a
48 48 c
49 49
50 50 --unified=2
51 51
52 52 $ hg diff --nodates -U 2 --color=always
53 53 \x1b[0;1mdiff -r cf9f4ba66af2 a\x1b[0m (esc)
54 54 \x1b[0;31;1m--- a/a\x1b[0m (esc)
55 55 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
56 56 \x1b[0;35m@@ -3,5 +3,5 @@\x1b[0m (esc)
57 57 a
58 58 a
59 59 \x1b[0;31m-b\x1b[0m (esc)
60 60 \x1b[0;32m+dd\x1b[0m (esc)
61 61 a
62 62 a
63 63
64 64 diffstat
65 65
66 66 $ hg diff --stat --color=always
67 67 a | 2 \x1b[0;32m+\x1b[0m\x1b[0;31m-\x1b[0m (esc)
68 68 1 files changed, 1 insertions(+), 1 deletions(-)
69 69 $ echo "record=" >> $HGRCPATH
70 70 $ echo "[ui]" >> $HGRCPATH
71 71 $ echo "interactive=true" >> $HGRCPATH
72 72 $ echo "[diff]" >> $HGRCPATH
73 73 $ echo "git=True" >> $HGRCPATH
74 74
75 75 #if execbit
76 76
77 77 record
78 78
79 79 $ chmod +x a
80 80 $ hg record --color=always -m moda a <<EOF
81 81 > y
82 82 > y
83 83 > EOF
84 84 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
85 85 \x1b[0;36;1mold mode 100644\x1b[0m (esc)
86 86 \x1b[0;36;1mnew mode 100755\x1b[0m (esc)
87 87 1 hunks, 1 lines changed
88 88 \x1b[0;33mexamine changes to 'a'? [Ynesfdaq?]\x1b[0m (esc)
89 89 \x1b[0;35m@@ -2,7 +2,7 @@\x1b[0m (esc)
90 90 c
91 91 a
92 92 a
93 93 \x1b[0;31m-b\x1b[0m (esc)
94 94 \x1b[0;32m+dd\x1b[0m (esc)
95 95 a
96 96 a
97 97 c
98 98 \x1b[0;33mrecord this change to 'a'? [Ynesfdaq?]\x1b[0m (esc)
99 99
100 100 $ echo "[extensions]" >> $HGRCPATH
101 101 $ echo "mq=" >> $HGRCPATH
102 102 $ hg rollback
103 103 repository tip rolled back to revision 0 (undo commit)
104 104 working directory now based on revision 0
105 105
106 106 qrecord
107 107
108 108 $ hg qrecord --color=always -m moda patch <<EOF
109 109 > y
110 110 > y
111 111 > EOF
112 112 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
113 113 \x1b[0;36;1mold mode 100644\x1b[0m (esc)
114 114 \x1b[0;36;1mnew mode 100755\x1b[0m (esc)
115 115 1 hunks, 1 lines changed
116 116 \x1b[0;33mexamine changes to 'a'? [Ynesfdaq?]\x1b[0m (esc)
117 117 \x1b[0;35m@@ -2,7 +2,7 @@\x1b[0m (esc)
118 118 c
119 119 a
120 120 a
121 121 \x1b[0;31m-b\x1b[0m (esc)
122 122 \x1b[0;32m+dd\x1b[0m (esc)
123 123 a
124 124 a
125 125 c
126 126 \x1b[0;33mrecord this change to 'a'? [Ynesfdaq?]\x1b[0m (esc)
127 127
128 128 $ hg qpop -a
129 129 popping patch
130 130 patch queue now empty
131 131
132 132 #endif
133 133
134 134 issue3712: test colorization of subrepo diff
135 135
136 136 $ hg init sub
137 137 $ echo b > sub/b
138 138 $ hg -R sub commit -Am 'create sub'
139 139 adding b
140 140 $ echo 'sub = sub' > .hgsub
141 141 $ hg add .hgsub
142 142 $ hg commit -m 'add subrepo sub'
143 143 $ echo aa >> a
144 144 $ echo bb >> sub/b
145 145
146 146 $ hg diff --color=always -S
147 147 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
148 148 \x1b[0;31;1m--- a/a\x1b[0m (esc)
149 149 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
150 150 \x1b[0;35m@@ -7,3 +7,4 @@\x1b[0m (esc)
151 151 a
152 152 c
153 153 c
154 154 \x1b[0;32m+aa\x1b[0m (esc)
155 155 \x1b[0;1mdiff --git a/sub/b b/sub/b\x1b[0m (esc)
156 156 \x1b[0;31;1m--- a/sub/b\x1b[0m (esc)
157 157 \x1b[0;32;1m+++ b/sub/b\x1b[0m (esc)
158 158 \x1b[0;35m@@ -1,1 +1,2 @@\x1b[0m (esc)
159 159 b
160 160 \x1b[0;32m+bb\x1b[0m (esc)
161 161
162 test tabs
163
164 $ cat >> a <<EOF
165 > one tab
166 > two tabs
167 > end tab
168 > mid tab
169 > all tabs
170 > EOF
171 $ hg diff --nodates --color=always
172 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
173 \x1b[0;31;1m--- a/a\x1b[0m (esc)
174 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
175 \x1b[0;35m@@ -7,3 +7,9 @@\x1b[0m (esc)
176 a
177 c
178 c
179 \x1b[0;32m+aa\x1b[0m (esc)
180 \x1b[0;32m+\x1b[0m \x1b[0;32mone tab\x1b[0m (esc)
181 \x1b[0;32m+\x1b[0m \x1b[0;32mtwo tabs\x1b[0m (esc)
182 \x1b[0;32m+end tab\x1b[0m\x1b[0;1;41m \x1b[0m (esc)
183 \x1b[0;32m+mid\x1b[0m \x1b[0;32mtab\x1b[0m (esc)
184 \x1b[0;32m+\x1b[0m \x1b[0;32mall\x1b[0m \x1b[0;32mtabs\x1b[0m\x1b[0;1;41m \x1b[0m (esc)
185 $ echo "[color]" >> $HGRCPATH
186 $ echo "diff.tab = bold magenta" >> $HGRCPATH
187 $ hg diff --nodates --color=always
188 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
189 \x1b[0;31;1m--- a/a\x1b[0m (esc)
190 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
191 \x1b[0;35m@@ -7,3 +7,9 @@\x1b[0m (esc)
192 a
193 c
194 c
195 \x1b[0;32m+aa\x1b[0m (esc)
196 \x1b[0;32m+\x1b[0m\x1b[0;1;35m \x1b[0m\x1b[0;32mone tab\x1b[0m (esc)
197 \x1b[0;32m+\x1b[0m\x1b[0;1;35m \x1b[0m\x1b[0;32mtwo tabs\x1b[0m (esc)
198 \x1b[0;32m+end tab\x1b[0m\x1b[0;1;41m \x1b[0m (esc)
199 \x1b[0;32m+mid\x1b[0m\x1b[0;1;35m \x1b[0m\x1b[0;32mtab\x1b[0m (esc)
200 \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)
201
162 202 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now