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