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