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