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