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