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