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