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