##// END OF EJS Templates
patch: clarify binary hunk parsing loop
Patrick Mezard -
r16567:aef3d0d4 default
parent child Browse files
Show More
@@ -1,1886 +1,1887 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 cStringIO, email.Parser, os, errno, re
10 10 import tempfile, zlib, shutil
11 11
12 12 from i18n import _
13 13 from node import hex, nullid, short
14 14 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
15 15 import context
16 16
17 17 gitre = re.compile('diff --git a/(.*) b/(.*)')
18 18
19 19 class PatchError(Exception):
20 20 pass
21 21
22 22
23 23 # public functions
24 24
25 25 def split(stream):
26 26 '''return an iterator of individual patches from a stream'''
27 27 def isheader(line, inheader):
28 28 if inheader and line[0] in (' ', '\t'):
29 29 # continuation
30 30 return True
31 31 if line[0] in (' ', '-', '+'):
32 32 # diff line - don't check for header pattern in there
33 33 return False
34 34 l = line.split(': ', 1)
35 35 return len(l) == 2 and ' ' not in l[0]
36 36
37 37 def chunk(lines):
38 38 return cStringIO.StringIO(''.join(lines))
39 39
40 40 def hgsplit(stream, cur):
41 41 inheader = True
42 42
43 43 for line in stream:
44 44 if not line.strip():
45 45 inheader = False
46 46 if not inheader and line.startswith('# HG changeset patch'):
47 47 yield chunk(cur)
48 48 cur = []
49 49 inheader = True
50 50
51 51 cur.append(line)
52 52
53 53 if cur:
54 54 yield chunk(cur)
55 55
56 56 def mboxsplit(stream, cur):
57 57 for line in stream:
58 58 if line.startswith('From '):
59 59 for c in split(chunk(cur[1:])):
60 60 yield c
61 61 cur = []
62 62
63 63 cur.append(line)
64 64
65 65 if cur:
66 66 for c in split(chunk(cur[1:])):
67 67 yield c
68 68
69 69 def mimesplit(stream, cur):
70 70 def msgfp(m):
71 71 fp = cStringIO.StringIO()
72 72 g = email.Generator.Generator(fp, mangle_from_=False)
73 73 g.flatten(m)
74 74 fp.seek(0)
75 75 return fp
76 76
77 77 for line in stream:
78 78 cur.append(line)
79 79 c = chunk(cur)
80 80
81 81 m = email.Parser.Parser().parse(c)
82 82 if not m.is_multipart():
83 83 yield msgfp(m)
84 84 else:
85 85 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
86 86 for part in m.walk():
87 87 ct = part.get_content_type()
88 88 if ct not in ok_types:
89 89 continue
90 90 yield msgfp(part)
91 91
92 92 def headersplit(stream, cur):
93 93 inheader = False
94 94
95 95 for line in stream:
96 96 if not inheader and isheader(line, inheader):
97 97 yield chunk(cur)
98 98 cur = []
99 99 inheader = True
100 100 if inheader and not isheader(line, inheader):
101 101 inheader = False
102 102
103 103 cur.append(line)
104 104
105 105 if cur:
106 106 yield chunk(cur)
107 107
108 108 def remainder(cur):
109 109 yield chunk(cur)
110 110
111 111 class fiter(object):
112 112 def __init__(self, fp):
113 113 self.fp = fp
114 114
115 115 def __iter__(self):
116 116 return self
117 117
118 118 def next(self):
119 119 l = self.fp.readline()
120 120 if not l:
121 121 raise StopIteration
122 122 return l
123 123
124 124 inheader = False
125 125 cur = []
126 126
127 127 mimeheaders = ['content-type']
128 128
129 129 if not util.safehasattr(stream, 'next'):
130 130 # http responses, for example, have readline but not next
131 131 stream = fiter(stream)
132 132
133 133 for line in stream:
134 134 cur.append(line)
135 135 if line.startswith('# HG changeset patch'):
136 136 return hgsplit(stream, cur)
137 137 elif line.startswith('From '):
138 138 return mboxsplit(stream, cur)
139 139 elif isheader(line, inheader):
140 140 inheader = True
141 141 if line.split(':', 1)[0].lower() in mimeheaders:
142 142 # let email parser handle this
143 143 return mimesplit(stream, cur)
144 144 elif line.startswith('--- ') and inheader:
145 145 # No evil headers seen by diff start, split by hand
146 146 return headersplit(stream, cur)
147 147 # Not enough info, keep reading
148 148
149 149 # if we are here, we have a very plain patch
150 150 return remainder(cur)
151 151
152 152 def extract(ui, fileobj):
153 153 '''extract patch from data read from fileobj.
154 154
155 155 patch can be a normal patch or contained in an email message.
156 156
157 157 return tuple (filename, message, user, date, branch, node, p1, p2).
158 158 Any item in the returned tuple can be None. If filename is None,
159 159 fileobj did not contain a patch. Caller must unlink filename when done.'''
160 160
161 161 # attempt to detect the start of a patch
162 162 # (this heuristic is borrowed from quilt)
163 163 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
164 164 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
165 165 r'---[ \t].*?^\+\+\+[ \t]|'
166 166 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
167 167
168 168 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
169 169 tmpfp = os.fdopen(fd, 'w')
170 170 try:
171 171 msg = email.Parser.Parser().parse(fileobj)
172 172
173 173 subject = msg['Subject']
174 174 user = msg['From']
175 175 if not subject and not user:
176 176 # Not an email, restore parsed headers if any
177 177 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
178 178
179 179 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
180 180 # should try to parse msg['Date']
181 181 date = None
182 182 nodeid = None
183 183 branch = None
184 184 parents = []
185 185
186 186 if subject:
187 187 if subject.startswith('[PATCH'):
188 188 pend = subject.find(']')
189 189 if pend >= 0:
190 190 subject = subject[pend + 1:].lstrip()
191 191 subject = re.sub(r'\n[ \t]+', ' ', subject)
192 192 ui.debug('Subject: %s\n' % subject)
193 193 if user:
194 194 ui.debug('From: %s\n' % user)
195 195 diffs_seen = 0
196 196 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
197 197 message = ''
198 198 for part in msg.walk():
199 199 content_type = part.get_content_type()
200 200 ui.debug('Content-Type: %s\n' % content_type)
201 201 if content_type not in ok_types:
202 202 continue
203 203 payload = part.get_payload(decode=True)
204 204 m = diffre.search(payload)
205 205 if m:
206 206 hgpatch = False
207 207 hgpatchheader = False
208 208 ignoretext = False
209 209
210 210 ui.debug('found patch at byte %d\n' % m.start(0))
211 211 diffs_seen += 1
212 212 cfp = cStringIO.StringIO()
213 213 for line in payload[:m.start(0)].splitlines():
214 214 if line.startswith('# HG changeset patch') and not hgpatch:
215 215 ui.debug('patch generated by hg export\n')
216 216 hgpatch = True
217 217 hgpatchheader = True
218 218 # drop earlier commit message content
219 219 cfp.seek(0)
220 220 cfp.truncate()
221 221 subject = None
222 222 elif hgpatchheader:
223 223 if line.startswith('# User '):
224 224 user = line[7:]
225 225 ui.debug('From: %s\n' % user)
226 226 elif line.startswith("# Date "):
227 227 date = line[7:]
228 228 elif line.startswith("# Branch "):
229 229 branch = line[9:]
230 230 elif line.startswith("# Node ID "):
231 231 nodeid = line[10:]
232 232 elif line.startswith("# Parent "):
233 233 parents.append(line[9:].lstrip())
234 234 elif not line.startswith("# "):
235 235 hgpatchheader = False
236 236 elif line == '---' and gitsendmail:
237 237 ignoretext = True
238 238 if not hgpatchheader and not ignoretext:
239 239 cfp.write(line)
240 240 cfp.write('\n')
241 241 message = cfp.getvalue()
242 242 if tmpfp:
243 243 tmpfp.write(payload)
244 244 if not payload.endswith('\n'):
245 245 tmpfp.write('\n')
246 246 elif not diffs_seen and message and content_type == 'text/plain':
247 247 message += '\n' + payload
248 248 except:
249 249 tmpfp.close()
250 250 os.unlink(tmpname)
251 251 raise
252 252
253 253 if subject and not message.startswith(subject):
254 254 message = '%s\n%s' % (subject, message)
255 255 tmpfp.close()
256 256 if not diffs_seen:
257 257 os.unlink(tmpname)
258 258 return None, message, user, date, branch, None, None, None
259 259 p1 = parents and parents.pop(0) or None
260 260 p2 = parents and parents.pop(0) or None
261 261 return tmpname, message, user, date, branch, nodeid, p1, p2
262 262
263 263 class patchmeta(object):
264 264 """Patched file metadata
265 265
266 266 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
267 267 or COPY. 'path' is patched file path. 'oldpath' is set to the
268 268 origin file when 'op' is either COPY or RENAME, None otherwise. If
269 269 file mode is changed, 'mode' is a tuple (islink, isexec) where
270 270 'islink' is True if the file is a symlink and 'isexec' is True if
271 271 the file is executable. Otherwise, 'mode' is None.
272 272 """
273 273 def __init__(self, path):
274 274 self.path = path
275 275 self.oldpath = None
276 276 self.mode = None
277 277 self.op = 'MODIFY'
278 278 self.binary = False
279 279
280 280 def setmode(self, mode):
281 281 islink = mode & 020000
282 282 isexec = mode & 0100
283 283 self.mode = (islink, isexec)
284 284
285 285 def copy(self):
286 286 other = patchmeta(self.path)
287 287 other.oldpath = self.oldpath
288 288 other.mode = self.mode
289 289 other.op = self.op
290 290 other.binary = self.binary
291 291 return other
292 292
293 293 def _ispatchinga(self, afile):
294 294 if afile == '/dev/null':
295 295 return self.op == 'ADD'
296 296 return afile == 'a/' + (self.oldpath or self.path)
297 297
298 298 def _ispatchingb(self, bfile):
299 299 if bfile == '/dev/null':
300 300 return self.op == 'DELETE'
301 301 return bfile == 'b/' + self.path
302 302
303 303 def ispatching(self, afile, bfile):
304 304 return self._ispatchinga(afile) and self._ispatchingb(bfile)
305 305
306 306 def __repr__(self):
307 307 return "<patchmeta %s %r>" % (self.op, self.path)
308 308
309 309 def readgitpatch(lr):
310 310 """extract git-style metadata about patches from <patchname>"""
311 311
312 312 # Filter patch for git information
313 313 gp = None
314 314 gitpatches = []
315 315 for line in lr:
316 316 line = line.rstrip(' \r\n')
317 317 if line.startswith('diff --git'):
318 318 m = gitre.match(line)
319 319 if m:
320 320 if gp:
321 321 gitpatches.append(gp)
322 322 dst = m.group(2)
323 323 gp = patchmeta(dst)
324 324 elif gp:
325 325 if line.startswith('--- '):
326 326 gitpatches.append(gp)
327 327 gp = None
328 328 continue
329 329 if line.startswith('rename from '):
330 330 gp.op = 'RENAME'
331 331 gp.oldpath = line[12:]
332 332 elif line.startswith('rename to '):
333 333 gp.path = line[10:]
334 334 elif line.startswith('copy from '):
335 335 gp.op = 'COPY'
336 336 gp.oldpath = line[10:]
337 337 elif line.startswith('copy to '):
338 338 gp.path = line[8:]
339 339 elif line.startswith('deleted file'):
340 340 gp.op = 'DELETE'
341 341 elif line.startswith('new file mode '):
342 342 gp.op = 'ADD'
343 343 gp.setmode(int(line[-6:], 8))
344 344 elif line.startswith('new mode '):
345 345 gp.setmode(int(line[-6:], 8))
346 346 elif line.startswith('GIT binary patch'):
347 347 gp.binary = True
348 348 if gp:
349 349 gitpatches.append(gp)
350 350
351 351 return gitpatches
352 352
353 353 class linereader(object):
354 354 # simple class to allow pushing lines back into the input stream
355 355 def __init__(self, fp):
356 356 self.fp = fp
357 357 self.buf = []
358 358
359 359 def push(self, line):
360 360 if line is not None:
361 361 self.buf.append(line)
362 362
363 363 def readline(self):
364 364 if self.buf:
365 365 l = self.buf[0]
366 366 del self.buf[0]
367 367 return l
368 368 return self.fp.readline()
369 369
370 370 def __iter__(self):
371 371 while True:
372 372 l = self.readline()
373 373 if not l:
374 374 break
375 375 yield l
376 376
377 377 class abstractbackend(object):
378 378 def __init__(self, ui):
379 379 self.ui = ui
380 380
381 381 def getfile(self, fname):
382 382 """Return target file data and flags as a (data, (islink,
383 383 isexec)) tuple.
384 384 """
385 385 raise NotImplementedError
386 386
387 387 def setfile(self, fname, data, mode, copysource):
388 388 """Write data to target file fname and set its mode. mode is a
389 389 (islink, isexec) tuple. If data is None, the file content should
390 390 be left unchanged. If the file is modified after being copied,
391 391 copysource is set to the original file name.
392 392 """
393 393 raise NotImplementedError
394 394
395 395 def unlink(self, fname):
396 396 """Unlink target file."""
397 397 raise NotImplementedError
398 398
399 399 def writerej(self, fname, failed, total, lines):
400 400 """Write rejected lines for fname. total is the number of hunks
401 401 which failed to apply and total the total number of hunks for this
402 402 files.
403 403 """
404 404 pass
405 405
406 406 def exists(self, fname):
407 407 raise NotImplementedError
408 408
409 409 class fsbackend(abstractbackend):
410 410 def __init__(self, ui, basedir):
411 411 super(fsbackend, self).__init__(ui)
412 412 self.opener = scmutil.opener(basedir)
413 413
414 414 def _join(self, f):
415 415 return os.path.join(self.opener.base, f)
416 416
417 417 def getfile(self, fname):
418 418 path = self._join(fname)
419 419 if os.path.islink(path):
420 420 return (os.readlink(path), (True, False))
421 421 isexec = False
422 422 try:
423 423 isexec = os.lstat(path).st_mode & 0100 != 0
424 424 except OSError, e:
425 425 if e.errno != errno.ENOENT:
426 426 raise
427 427 return (self.opener.read(fname), (False, isexec))
428 428
429 429 def setfile(self, fname, data, mode, copysource):
430 430 islink, isexec = mode
431 431 if data is None:
432 432 util.setflags(self._join(fname), islink, isexec)
433 433 return
434 434 if islink:
435 435 self.opener.symlink(data, fname)
436 436 else:
437 437 self.opener.write(fname, data)
438 438 if isexec:
439 439 util.setflags(self._join(fname), False, True)
440 440
441 441 def unlink(self, fname):
442 442 try:
443 443 util.unlinkpath(self._join(fname))
444 444 except OSError, inst:
445 445 if inst.errno != errno.ENOENT:
446 446 raise
447 447
448 448 def writerej(self, fname, failed, total, lines):
449 449 fname = fname + ".rej"
450 450 self.ui.warn(
451 451 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
452 452 (failed, total, fname))
453 453 fp = self.opener(fname, 'w')
454 454 fp.writelines(lines)
455 455 fp.close()
456 456
457 457 def exists(self, fname):
458 458 return os.path.lexists(self._join(fname))
459 459
460 460 class workingbackend(fsbackend):
461 461 def __init__(self, ui, repo, similarity):
462 462 super(workingbackend, self).__init__(ui, repo.root)
463 463 self.repo = repo
464 464 self.similarity = similarity
465 465 self.removed = set()
466 466 self.changed = set()
467 467 self.copied = []
468 468
469 469 def _checkknown(self, fname):
470 470 if self.repo.dirstate[fname] == '?' and self.exists(fname):
471 471 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
472 472
473 473 def setfile(self, fname, data, mode, copysource):
474 474 self._checkknown(fname)
475 475 super(workingbackend, self).setfile(fname, data, mode, copysource)
476 476 if copysource is not None:
477 477 self.copied.append((copysource, fname))
478 478 self.changed.add(fname)
479 479
480 480 def unlink(self, fname):
481 481 self._checkknown(fname)
482 482 super(workingbackend, self).unlink(fname)
483 483 self.removed.add(fname)
484 484 self.changed.add(fname)
485 485
486 486 def close(self):
487 487 wctx = self.repo[None]
488 488 addremoved = set(self.changed)
489 489 for src, dst in self.copied:
490 490 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
491 491 if self.removed:
492 492 wctx.forget(sorted(self.removed))
493 493 for f in self.removed:
494 494 if f not in self.repo.dirstate:
495 495 # File was deleted and no longer belongs to the
496 496 # dirstate, it was probably marked added then
497 497 # deleted, and should not be considered by
498 498 # addremove().
499 499 addremoved.discard(f)
500 500 if addremoved:
501 501 cwd = self.repo.getcwd()
502 502 if cwd:
503 503 addremoved = [util.pathto(self.repo.root, cwd, f)
504 504 for f in addremoved]
505 505 scmutil.addremove(self.repo, addremoved, similarity=self.similarity)
506 506 return sorted(self.changed)
507 507
508 508 class filestore(object):
509 509 def __init__(self, maxsize=None):
510 510 self.opener = None
511 511 self.files = {}
512 512 self.created = 0
513 513 self.maxsize = maxsize
514 514 if self.maxsize is None:
515 515 self.maxsize = 4*(2**20)
516 516 self.size = 0
517 517 self.data = {}
518 518
519 519 def setfile(self, fname, data, mode, copied=None):
520 520 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
521 521 self.data[fname] = (data, mode, copied)
522 522 self.size += len(data)
523 523 else:
524 524 if self.opener is None:
525 525 root = tempfile.mkdtemp(prefix='hg-patch-')
526 526 self.opener = scmutil.opener(root)
527 527 # Avoid filename issues with these simple names
528 528 fn = str(self.created)
529 529 self.opener.write(fn, data)
530 530 self.created += 1
531 531 self.files[fname] = (fn, mode, copied)
532 532
533 533 def getfile(self, fname):
534 534 if fname in self.data:
535 535 return self.data[fname]
536 536 if not self.opener or fname not in self.files:
537 537 raise IOError()
538 538 fn, mode, copied = self.files[fname]
539 539 return self.opener.read(fn), mode, copied
540 540
541 541 def close(self):
542 542 if self.opener:
543 543 shutil.rmtree(self.opener.base)
544 544
545 545 class repobackend(abstractbackend):
546 546 def __init__(self, ui, repo, ctx, store):
547 547 super(repobackend, self).__init__(ui)
548 548 self.repo = repo
549 549 self.ctx = ctx
550 550 self.store = store
551 551 self.changed = set()
552 552 self.removed = set()
553 553 self.copied = {}
554 554
555 555 def _checkknown(self, fname):
556 556 if fname not in self.ctx:
557 557 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
558 558
559 559 def getfile(self, fname):
560 560 try:
561 561 fctx = self.ctx[fname]
562 562 except error.LookupError:
563 563 raise IOError()
564 564 flags = fctx.flags()
565 565 return fctx.data(), ('l' in flags, 'x' in flags)
566 566
567 567 def setfile(self, fname, data, mode, copysource):
568 568 if copysource:
569 569 self._checkknown(copysource)
570 570 if data is None:
571 571 data = self.ctx[fname].data()
572 572 self.store.setfile(fname, data, mode, copysource)
573 573 self.changed.add(fname)
574 574 if copysource:
575 575 self.copied[fname] = copysource
576 576
577 577 def unlink(self, fname):
578 578 self._checkknown(fname)
579 579 self.removed.add(fname)
580 580
581 581 def exists(self, fname):
582 582 return fname in self.ctx
583 583
584 584 def close(self):
585 585 return self.changed | self.removed
586 586
587 587 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
588 588 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
589 589 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
590 590 eolmodes = ['strict', 'crlf', 'lf', 'auto']
591 591
592 592 class patchfile(object):
593 593 def __init__(self, ui, gp, backend, store, eolmode='strict'):
594 594 self.fname = gp.path
595 595 self.eolmode = eolmode
596 596 self.eol = None
597 597 self.backend = backend
598 598 self.ui = ui
599 599 self.lines = []
600 600 self.exists = False
601 601 self.missing = True
602 602 self.mode = gp.mode
603 603 self.copysource = gp.oldpath
604 604 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
605 605 self.remove = gp.op == 'DELETE'
606 606 try:
607 607 if self.copysource is None:
608 608 data, mode = backend.getfile(self.fname)
609 609 self.exists = True
610 610 else:
611 611 data, mode = store.getfile(self.copysource)[:2]
612 612 self.exists = backend.exists(self.fname)
613 613 self.missing = False
614 614 if data:
615 615 self.lines = mdiff.splitnewlines(data)
616 616 if self.mode is None:
617 617 self.mode = mode
618 618 if self.lines:
619 619 # Normalize line endings
620 620 if self.lines[0].endswith('\r\n'):
621 621 self.eol = '\r\n'
622 622 elif self.lines[0].endswith('\n'):
623 623 self.eol = '\n'
624 624 if eolmode != 'strict':
625 625 nlines = []
626 626 for l in self.lines:
627 627 if l.endswith('\r\n'):
628 628 l = l[:-2] + '\n'
629 629 nlines.append(l)
630 630 self.lines = nlines
631 631 except IOError:
632 632 if self.create:
633 633 self.missing = False
634 634 if self.mode is None:
635 635 self.mode = (False, False)
636 636 if self.missing:
637 637 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
638 638
639 639 self.hash = {}
640 640 self.dirty = 0
641 641 self.offset = 0
642 642 self.skew = 0
643 643 self.rej = []
644 644 self.fileprinted = False
645 645 self.printfile(False)
646 646 self.hunks = 0
647 647
648 648 def writelines(self, fname, lines, mode):
649 649 if self.eolmode == 'auto':
650 650 eol = self.eol
651 651 elif self.eolmode == 'crlf':
652 652 eol = '\r\n'
653 653 else:
654 654 eol = '\n'
655 655
656 656 if self.eolmode != 'strict' and eol and eol != '\n':
657 657 rawlines = []
658 658 for l in lines:
659 659 if l and l[-1] == '\n':
660 660 l = l[:-1] + eol
661 661 rawlines.append(l)
662 662 lines = rawlines
663 663
664 664 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
665 665
666 666 def printfile(self, warn):
667 667 if self.fileprinted:
668 668 return
669 669 if warn or self.ui.verbose:
670 670 self.fileprinted = True
671 671 s = _("patching file %s\n") % self.fname
672 672 if warn:
673 673 self.ui.warn(s)
674 674 else:
675 675 self.ui.note(s)
676 676
677 677
678 678 def findlines(self, l, linenum):
679 679 # looks through the hash and finds candidate lines. The
680 680 # result is a list of line numbers sorted based on distance
681 681 # from linenum
682 682
683 683 cand = self.hash.get(l, [])
684 684 if len(cand) > 1:
685 685 # resort our list of potentials forward then back.
686 686 cand.sort(key=lambda x: abs(x - linenum))
687 687 return cand
688 688
689 689 def write_rej(self):
690 690 # our rejects are a little different from patch(1). This always
691 691 # creates rejects in the same form as the original patch. A file
692 692 # header is inserted so that you can run the reject through patch again
693 693 # without having to type the filename.
694 694 if not self.rej:
695 695 return
696 696 base = os.path.basename(self.fname)
697 697 lines = ["--- %s\n+++ %s\n" % (base, base)]
698 698 for x in self.rej:
699 699 for l in x.hunk:
700 700 lines.append(l)
701 701 if l[-1] != '\n':
702 702 lines.append("\n\ No newline at end of file\n")
703 703 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
704 704
705 705 def apply(self, h):
706 706 if not h.complete():
707 707 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
708 708 (h.number, h.desc, len(h.a), h.lena, len(h.b),
709 709 h.lenb))
710 710
711 711 self.hunks += 1
712 712
713 713 if self.missing:
714 714 self.rej.append(h)
715 715 return -1
716 716
717 717 if self.exists and self.create:
718 718 if self.copysource:
719 719 self.ui.warn(_("cannot create %s: destination already "
720 720 "exists\n" % self.fname))
721 721 else:
722 722 self.ui.warn(_("file %s already exists\n") % self.fname)
723 723 self.rej.append(h)
724 724 return -1
725 725
726 726 if isinstance(h, binhunk):
727 727 if self.remove:
728 728 self.backend.unlink(self.fname)
729 729 else:
730 730 self.lines[:] = h.new()
731 731 self.offset += len(h.new())
732 732 self.dirty = True
733 733 return 0
734 734
735 735 horig = h
736 736 if (self.eolmode in ('crlf', 'lf')
737 737 or self.eolmode == 'auto' and self.eol):
738 738 # If new eols are going to be normalized, then normalize
739 739 # hunk data before patching. Otherwise, preserve input
740 740 # line-endings.
741 741 h = h.getnormalized()
742 742
743 743 # fast case first, no offsets, no fuzz
744 744 old, oldstart, new, newstart = h.fuzzit(0, False)
745 745 oldstart += self.offset
746 746 orig_start = oldstart
747 747 # if there's skew we want to emit the "(offset %d lines)" even
748 748 # when the hunk cleanly applies at start + skew, so skip the
749 749 # fast case code
750 750 if (self.skew == 0 and
751 751 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
752 752 if self.remove:
753 753 self.backend.unlink(self.fname)
754 754 else:
755 755 self.lines[oldstart:oldstart + len(old)] = new
756 756 self.offset += len(new) - len(old)
757 757 self.dirty = True
758 758 return 0
759 759
760 760 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
761 761 self.hash = {}
762 762 for x, s in enumerate(self.lines):
763 763 self.hash.setdefault(s, []).append(x)
764 764
765 765 for fuzzlen in xrange(3):
766 766 for toponly in [True, False]:
767 767 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
768 768 oldstart = oldstart + self.offset + self.skew
769 769 oldstart = min(oldstart, len(self.lines))
770 770 if old:
771 771 cand = self.findlines(old[0][1:], oldstart)
772 772 else:
773 773 # Only adding lines with no or fuzzed context, just
774 774 # take the skew in account
775 775 cand = [oldstart]
776 776
777 777 for l in cand:
778 778 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
779 779 self.lines[l : l + len(old)] = new
780 780 self.offset += len(new) - len(old)
781 781 self.skew = l - orig_start
782 782 self.dirty = True
783 783 offset = l - orig_start - fuzzlen
784 784 if fuzzlen:
785 785 msg = _("Hunk #%d succeeded at %d "
786 786 "with fuzz %d "
787 787 "(offset %d lines).\n")
788 788 self.printfile(True)
789 789 self.ui.warn(msg %
790 790 (h.number, l + 1, fuzzlen, offset))
791 791 else:
792 792 msg = _("Hunk #%d succeeded at %d "
793 793 "(offset %d lines).\n")
794 794 self.ui.note(msg % (h.number, l + 1, offset))
795 795 return fuzzlen
796 796 self.printfile(True)
797 797 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
798 798 self.rej.append(horig)
799 799 return -1
800 800
801 801 def close(self):
802 802 if self.dirty:
803 803 self.writelines(self.fname, self.lines, self.mode)
804 804 self.write_rej()
805 805 return len(self.rej)
806 806
807 807 class hunk(object):
808 808 def __init__(self, desc, num, lr, context):
809 809 self.number = num
810 810 self.desc = desc
811 811 self.hunk = [desc]
812 812 self.a = []
813 813 self.b = []
814 814 self.starta = self.lena = None
815 815 self.startb = self.lenb = None
816 816 if lr is not None:
817 817 if context:
818 818 self.read_context_hunk(lr)
819 819 else:
820 820 self.read_unified_hunk(lr)
821 821
822 822 def getnormalized(self):
823 823 """Return a copy with line endings normalized to LF."""
824 824
825 825 def normalize(lines):
826 826 nlines = []
827 827 for line in lines:
828 828 if line.endswith('\r\n'):
829 829 line = line[:-2] + '\n'
830 830 nlines.append(line)
831 831 return nlines
832 832
833 833 # Dummy object, it is rebuilt manually
834 834 nh = hunk(self.desc, self.number, None, None)
835 835 nh.number = self.number
836 836 nh.desc = self.desc
837 837 nh.hunk = self.hunk
838 838 nh.a = normalize(self.a)
839 839 nh.b = normalize(self.b)
840 840 nh.starta = self.starta
841 841 nh.startb = self.startb
842 842 nh.lena = self.lena
843 843 nh.lenb = self.lenb
844 844 return nh
845 845
846 846 def read_unified_hunk(self, lr):
847 847 m = unidesc.match(self.desc)
848 848 if not m:
849 849 raise PatchError(_("bad hunk #%d") % self.number)
850 850 self.starta, self.lena, self.startb, self.lenb = m.groups()
851 851 if self.lena is None:
852 852 self.lena = 1
853 853 else:
854 854 self.lena = int(self.lena)
855 855 if self.lenb is None:
856 856 self.lenb = 1
857 857 else:
858 858 self.lenb = int(self.lenb)
859 859 self.starta = int(self.starta)
860 860 self.startb = int(self.startb)
861 861 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
862 862 # if we hit eof before finishing out the hunk, the last line will
863 863 # be zero length. Lets try to fix it up.
864 864 while len(self.hunk[-1]) == 0:
865 865 del self.hunk[-1]
866 866 del self.a[-1]
867 867 del self.b[-1]
868 868 self.lena -= 1
869 869 self.lenb -= 1
870 870 self._fixnewline(lr)
871 871
872 872 def read_context_hunk(self, lr):
873 873 self.desc = lr.readline()
874 874 m = contextdesc.match(self.desc)
875 875 if not m:
876 876 raise PatchError(_("bad hunk #%d") % self.number)
877 877 self.starta, aend = m.groups()
878 878 self.starta = int(self.starta)
879 879 if aend is None:
880 880 aend = self.starta
881 881 self.lena = int(aend) - self.starta
882 882 if self.starta:
883 883 self.lena += 1
884 884 for x in xrange(self.lena):
885 885 l = lr.readline()
886 886 if l.startswith('---'):
887 887 # lines addition, old block is empty
888 888 lr.push(l)
889 889 break
890 890 s = l[2:]
891 891 if l.startswith('- ') or l.startswith('! '):
892 892 u = '-' + s
893 893 elif l.startswith(' '):
894 894 u = ' ' + s
895 895 else:
896 896 raise PatchError(_("bad hunk #%d old text line %d") %
897 897 (self.number, x))
898 898 self.a.append(u)
899 899 self.hunk.append(u)
900 900
901 901 l = lr.readline()
902 902 if l.startswith('\ '):
903 903 s = self.a[-1][:-1]
904 904 self.a[-1] = s
905 905 self.hunk[-1] = s
906 906 l = lr.readline()
907 907 m = contextdesc.match(l)
908 908 if not m:
909 909 raise PatchError(_("bad hunk #%d") % self.number)
910 910 self.startb, bend = m.groups()
911 911 self.startb = int(self.startb)
912 912 if bend is None:
913 913 bend = self.startb
914 914 self.lenb = int(bend) - self.startb
915 915 if self.startb:
916 916 self.lenb += 1
917 917 hunki = 1
918 918 for x in xrange(self.lenb):
919 919 l = lr.readline()
920 920 if l.startswith('\ '):
921 921 # XXX: the only way to hit this is with an invalid line range.
922 922 # The no-eol marker is not counted in the line range, but I
923 923 # guess there are diff(1) out there which behave differently.
924 924 s = self.b[-1][:-1]
925 925 self.b[-1] = s
926 926 self.hunk[hunki - 1] = s
927 927 continue
928 928 if not l:
929 929 # line deletions, new block is empty and we hit EOF
930 930 lr.push(l)
931 931 break
932 932 s = l[2:]
933 933 if l.startswith('+ ') or l.startswith('! '):
934 934 u = '+' + s
935 935 elif l.startswith(' '):
936 936 u = ' ' + s
937 937 elif len(self.b) == 0:
938 938 # line deletions, new block is empty
939 939 lr.push(l)
940 940 break
941 941 else:
942 942 raise PatchError(_("bad hunk #%d old text line %d") %
943 943 (self.number, x))
944 944 self.b.append(s)
945 945 while True:
946 946 if hunki >= len(self.hunk):
947 947 h = ""
948 948 else:
949 949 h = self.hunk[hunki]
950 950 hunki += 1
951 951 if h == u:
952 952 break
953 953 elif h.startswith('-'):
954 954 continue
955 955 else:
956 956 self.hunk.insert(hunki - 1, u)
957 957 break
958 958
959 959 if not self.a:
960 960 # this happens when lines were only added to the hunk
961 961 for x in self.hunk:
962 962 if x.startswith('-') or x.startswith(' '):
963 963 self.a.append(x)
964 964 if not self.b:
965 965 # this happens when lines were only deleted from the hunk
966 966 for x in self.hunk:
967 967 if x.startswith('+') or x.startswith(' '):
968 968 self.b.append(x[1:])
969 969 # @@ -start,len +start,len @@
970 970 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
971 971 self.startb, self.lenb)
972 972 self.hunk[0] = self.desc
973 973 self._fixnewline(lr)
974 974
975 975 def _fixnewline(self, lr):
976 976 l = lr.readline()
977 977 if l.startswith('\ '):
978 978 diffhelpers.fix_newline(self.hunk, self.a, self.b)
979 979 else:
980 980 lr.push(l)
981 981
982 982 def complete(self):
983 983 return len(self.a) == self.lena and len(self.b) == self.lenb
984 984
985 985 def _fuzzit(self, old, new, fuzz, toponly):
986 986 # this removes context lines from the top and bottom of list 'l'. It
987 987 # checks the hunk to make sure only context lines are removed, and then
988 988 # returns a new shortened list of lines.
989 989 fuzz = min(fuzz, len(old))
990 990 if fuzz:
991 991 top = 0
992 992 bot = 0
993 993 hlen = len(self.hunk)
994 994 for x in xrange(hlen - 1):
995 995 # the hunk starts with the @@ line, so use x+1
996 996 if self.hunk[x + 1][0] == ' ':
997 997 top += 1
998 998 else:
999 999 break
1000 1000 if not toponly:
1001 1001 for x in xrange(hlen - 1):
1002 1002 if self.hunk[hlen - bot - 1][0] == ' ':
1003 1003 bot += 1
1004 1004 else:
1005 1005 break
1006 1006
1007 1007 bot = min(fuzz, bot)
1008 1008 top = min(fuzz, top)
1009 1009 return old[top:len(old)-bot], new[top:len(new)-bot], top
1010 1010 return old, new, 0
1011 1011
1012 1012 def fuzzit(self, fuzz, toponly):
1013 1013 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1014 1014 oldstart = self.starta + top
1015 1015 newstart = self.startb + top
1016 1016 # zero length hunk ranges already have their start decremented
1017 1017 if self.lena:
1018 1018 oldstart -= 1
1019 1019 if self.lenb:
1020 1020 newstart -= 1
1021 1021 return old, oldstart, new, newstart
1022 1022
1023 1023 class binhunk(object):
1024 1024 'A binary patch file. Only understands literals so far.'
1025 1025 def __init__(self, lr, fname):
1026 1026 self.text = None
1027 1027 self.hunk = ['GIT binary patch\n']
1028 1028 self._fname = fname
1029 1029 self._read(lr)
1030 1030
1031 1031 def complete(self):
1032 1032 return self.text is not None
1033 1033
1034 1034 def new(self):
1035 1035 return [self.text]
1036 1036
1037 1037 def _read(self, lr):
1038 1038 def getline(lr, hunk):
1039 1039 l = lr.readline()
1040 1040 hunk.append(l)
1041 1041 return l.rstrip('\r\n')
1042 1042
1043 line = getline(lr, self.hunk)
1044 while line and not line.startswith('literal '):
1043 while True:
1045 1044 line = getline(lr, self.hunk)
1046 if not line:
1047 raise PatchError(_('could not extract "%s" binary data')
1048 % self._fname)
1045 if not line:
1046 raise PatchError(_('could not extract "%s" binary data')
1047 % self._fname)
1048 if line.startswith('literal '):
1049 break
1049 1050 size = int(line[8:].rstrip())
1050 1051 dec = []
1051 1052 line = getline(lr, self.hunk)
1052 1053 while len(line) > 1:
1053 1054 l = line[0]
1054 1055 if l <= 'Z' and l >= 'A':
1055 1056 l = ord(l) - ord('A') + 1
1056 1057 else:
1057 1058 l = ord(l) - ord('a') + 27
1058 1059 try:
1059 1060 dec.append(base85.b85decode(line[1:])[:l])
1060 1061 except ValueError, e:
1061 1062 raise PatchError(_('could not decode "%s" binary patch: %s')
1062 1063 % (self._fname, str(e)))
1063 1064 line = getline(lr, self.hunk)
1064 1065 text = zlib.decompress(''.join(dec))
1065 1066 if len(text) != size:
1066 1067 raise PatchError(_('"%s" length is %d bytes, should be %d')
1067 1068 % (self._fname, len(text), size))
1068 1069 self.text = text
1069 1070
1070 1071 def parsefilename(str):
1071 1072 # --- filename \t|space stuff
1072 1073 s = str[4:].rstrip('\r\n')
1073 1074 i = s.find('\t')
1074 1075 if i < 0:
1075 1076 i = s.find(' ')
1076 1077 if i < 0:
1077 1078 return s
1078 1079 return s[:i]
1079 1080
1080 1081 def pathstrip(path, strip):
1081 1082 pathlen = len(path)
1082 1083 i = 0
1083 1084 if strip == 0:
1084 1085 return '', path.rstrip()
1085 1086 count = strip
1086 1087 while count > 0:
1087 1088 i = path.find('/', i)
1088 1089 if i == -1:
1089 1090 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1090 1091 (count, strip, path))
1091 1092 i += 1
1092 1093 # consume '//' in the path
1093 1094 while i < pathlen - 1 and path[i] == '/':
1094 1095 i += 1
1095 1096 count -= 1
1096 1097 return path[:i].lstrip(), path[i:].rstrip()
1097 1098
1098 1099 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip):
1099 1100 nulla = afile_orig == "/dev/null"
1100 1101 nullb = bfile_orig == "/dev/null"
1101 1102 create = nulla and hunk.starta == 0 and hunk.lena == 0
1102 1103 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1103 1104 abase, afile = pathstrip(afile_orig, strip)
1104 1105 gooda = not nulla and backend.exists(afile)
1105 1106 bbase, bfile = pathstrip(bfile_orig, strip)
1106 1107 if afile == bfile:
1107 1108 goodb = gooda
1108 1109 else:
1109 1110 goodb = not nullb and backend.exists(bfile)
1110 1111 missing = not goodb and not gooda and not create
1111 1112
1112 1113 # some diff programs apparently produce patches where the afile is
1113 1114 # not /dev/null, but afile starts with bfile
1114 1115 abasedir = afile[:afile.rfind('/') + 1]
1115 1116 bbasedir = bfile[:bfile.rfind('/') + 1]
1116 1117 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1117 1118 and hunk.starta == 0 and hunk.lena == 0):
1118 1119 create = True
1119 1120 missing = False
1120 1121
1121 1122 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1122 1123 # diff is between a file and its backup. In this case, the original
1123 1124 # file should be patched (see original mpatch code).
1124 1125 isbackup = (abase == bbase and bfile.startswith(afile))
1125 1126 fname = None
1126 1127 if not missing:
1127 1128 if gooda and goodb:
1128 1129 fname = isbackup and afile or bfile
1129 1130 elif gooda:
1130 1131 fname = afile
1131 1132
1132 1133 if not fname:
1133 1134 if not nullb:
1134 1135 fname = isbackup and afile or bfile
1135 1136 elif not nulla:
1136 1137 fname = afile
1137 1138 else:
1138 1139 raise PatchError(_("undefined source and destination files"))
1139 1140
1140 1141 gp = patchmeta(fname)
1141 1142 if create:
1142 1143 gp.op = 'ADD'
1143 1144 elif remove:
1144 1145 gp.op = 'DELETE'
1145 1146 return gp
1146 1147
1147 1148 def scangitpatch(lr, firstline):
1148 1149 """
1149 1150 Git patches can emit:
1150 1151 - rename a to b
1151 1152 - change b
1152 1153 - copy a to c
1153 1154 - change c
1154 1155
1155 1156 We cannot apply this sequence as-is, the renamed 'a' could not be
1156 1157 found for it would have been renamed already. And we cannot copy
1157 1158 from 'b' instead because 'b' would have been changed already. So
1158 1159 we scan the git patch for copy and rename commands so we can
1159 1160 perform the copies ahead of time.
1160 1161 """
1161 1162 pos = 0
1162 1163 try:
1163 1164 pos = lr.fp.tell()
1164 1165 fp = lr.fp
1165 1166 except IOError:
1166 1167 fp = cStringIO.StringIO(lr.fp.read())
1167 1168 gitlr = linereader(fp)
1168 1169 gitlr.push(firstline)
1169 1170 gitpatches = readgitpatch(gitlr)
1170 1171 fp.seek(pos)
1171 1172 return gitpatches
1172 1173
1173 1174 def iterhunks(fp):
1174 1175 """Read a patch and yield the following events:
1175 1176 - ("file", afile, bfile, firsthunk): select a new target file.
1176 1177 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1177 1178 "file" event.
1178 1179 - ("git", gitchanges): current diff is in git format, gitchanges
1179 1180 maps filenames to gitpatch records. Unique event.
1180 1181 """
1181 1182 afile = ""
1182 1183 bfile = ""
1183 1184 state = None
1184 1185 hunknum = 0
1185 1186 emitfile = newfile = False
1186 1187 gitpatches = None
1187 1188
1188 1189 # our states
1189 1190 BFILE = 1
1190 1191 context = None
1191 1192 lr = linereader(fp)
1192 1193
1193 1194 while True:
1194 1195 x = lr.readline()
1195 1196 if not x:
1196 1197 break
1197 1198 if state == BFILE and (
1198 1199 (not context and x[0] == '@')
1199 1200 or (context is not False and x.startswith('***************'))
1200 1201 or x.startswith('GIT binary patch')):
1201 1202 gp = None
1202 1203 if (gitpatches and
1203 1204 gitpatches[-1].ispatching(afile, bfile)):
1204 1205 gp = gitpatches.pop()
1205 1206 if x.startswith('GIT binary patch'):
1206 1207 h = binhunk(lr, gp.path)
1207 1208 else:
1208 1209 if context is None and x.startswith('***************'):
1209 1210 context = True
1210 1211 h = hunk(x, hunknum + 1, lr, context)
1211 1212 hunknum += 1
1212 1213 if emitfile:
1213 1214 emitfile = False
1214 1215 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1215 1216 yield 'hunk', h
1216 1217 elif x.startswith('diff --git'):
1217 1218 m = gitre.match(x.rstrip(' \r\n'))
1218 1219 if not m:
1219 1220 continue
1220 1221 if gitpatches is None:
1221 1222 # scan whole input for git metadata
1222 1223 gitpatches = scangitpatch(lr, x)
1223 1224 yield 'git', [g.copy() for g in gitpatches
1224 1225 if g.op in ('COPY', 'RENAME')]
1225 1226 gitpatches.reverse()
1226 1227 afile = 'a/' + m.group(1)
1227 1228 bfile = 'b/' + m.group(2)
1228 1229 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1229 1230 gp = gitpatches.pop()
1230 1231 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1231 1232 if not gitpatches:
1232 1233 raise PatchError(_('failed to synchronize metadata for "%s"')
1233 1234 % afile[2:])
1234 1235 gp = gitpatches[-1]
1235 1236 newfile = True
1236 1237 elif x.startswith('---'):
1237 1238 # check for a unified diff
1238 1239 l2 = lr.readline()
1239 1240 if not l2.startswith('+++'):
1240 1241 lr.push(l2)
1241 1242 continue
1242 1243 newfile = True
1243 1244 context = False
1244 1245 afile = parsefilename(x)
1245 1246 bfile = parsefilename(l2)
1246 1247 elif x.startswith('***'):
1247 1248 # check for a context diff
1248 1249 l2 = lr.readline()
1249 1250 if not l2.startswith('---'):
1250 1251 lr.push(l2)
1251 1252 continue
1252 1253 l3 = lr.readline()
1253 1254 lr.push(l3)
1254 1255 if not l3.startswith("***************"):
1255 1256 lr.push(l2)
1256 1257 continue
1257 1258 newfile = True
1258 1259 context = True
1259 1260 afile = parsefilename(x)
1260 1261 bfile = parsefilename(l2)
1261 1262
1262 1263 if newfile:
1263 1264 newfile = False
1264 1265 emitfile = True
1265 1266 state = BFILE
1266 1267 hunknum = 0
1267 1268
1268 1269 while gitpatches:
1269 1270 gp = gitpatches.pop()
1270 1271 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1271 1272
1272 1273 def applydiff(ui, fp, backend, store, strip=1, eolmode='strict'):
1273 1274 """Reads a patch from fp and tries to apply it.
1274 1275
1275 1276 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1276 1277 there was any fuzz.
1277 1278
1278 1279 If 'eolmode' is 'strict', the patch content and patched file are
1279 1280 read in binary mode. Otherwise, line endings are ignored when
1280 1281 patching then normalized according to 'eolmode'.
1281 1282 """
1282 1283 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1283 1284 eolmode=eolmode)
1284 1285
1285 1286 def _applydiff(ui, fp, patcher, backend, store, strip=1,
1286 1287 eolmode='strict'):
1287 1288
1288 1289 def pstrip(p):
1289 1290 return pathstrip(p, strip - 1)[1]
1290 1291
1291 1292 rejects = 0
1292 1293 err = 0
1293 1294 current_file = None
1294 1295
1295 1296 for state, values in iterhunks(fp):
1296 1297 if state == 'hunk':
1297 1298 if not current_file:
1298 1299 continue
1299 1300 ret = current_file.apply(values)
1300 1301 if ret > 0:
1301 1302 err = 1
1302 1303 elif state == 'file':
1303 1304 if current_file:
1304 1305 rejects += current_file.close()
1305 1306 current_file = None
1306 1307 afile, bfile, first_hunk, gp = values
1307 1308 if gp:
1308 1309 gp.path = pstrip(gp.path)
1309 1310 if gp.oldpath:
1310 1311 gp.oldpath = pstrip(gp.oldpath)
1311 1312 else:
1312 1313 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1313 1314 if gp.op == 'RENAME':
1314 1315 backend.unlink(gp.oldpath)
1315 1316 if not first_hunk:
1316 1317 if gp.op == 'DELETE':
1317 1318 backend.unlink(gp.path)
1318 1319 continue
1319 1320 data, mode = None, None
1320 1321 if gp.op in ('RENAME', 'COPY'):
1321 1322 data, mode = store.getfile(gp.oldpath)[:2]
1322 1323 if gp.mode:
1323 1324 mode = gp.mode
1324 1325 if gp.op == 'ADD':
1325 1326 # Added files without content have no hunk and
1326 1327 # must be created
1327 1328 data = ''
1328 1329 if data or mode:
1329 1330 if (gp.op in ('ADD', 'RENAME', 'COPY')
1330 1331 and backend.exists(gp.path)):
1331 1332 raise PatchError(_("cannot create %s: destination "
1332 1333 "already exists") % gp.path)
1333 1334 backend.setfile(gp.path, data, mode, gp.oldpath)
1334 1335 continue
1335 1336 try:
1336 1337 current_file = patcher(ui, gp, backend, store,
1337 1338 eolmode=eolmode)
1338 1339 except PatchError, inst:
1339 1340 ui.warn(str(inst) + '\n')
1340 1341 current_file = None
1341 1342 rejects += 1
1342 1343 continue
1343 1344 elif state == 'git':
1344 1345 for gp in values:
1345 1346 path = pstrip(gp.oldpath)
1346 1347 data, mode = backend.getfile(path)
1347 1348 store.setfile(path, data, mode)
1348 1349 else:
1349 1350 raise util.Abort(_('unsupported parser state: %s') % state)
1350 1351
1351 1352 if current_file:
1352 1353 rejects += current_file.close()
1353 1354
1354 1355 if rejects:
1355 1356 return -1
1356 1357 return err
1357 1358
1358 1359 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1359 1360 similarity):
1360 1361 """use <patcher> to apply <patchname> to the working directory.
1361 1362 returns whether patch was applied with fuzz factor."""
1362 1363
1363 1364 fuzz = False
1364 1365 args = []
1365 1366 cwd = repo.root
1366 1367 if cwd:
1367 1368 args.append('-d %s' % util.shellquote(cwd))
1368 1369 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1369 1370 util.shellquote(patchname)))
1370 1371 try:
1371 1372 for line in fp:
1372 1373 line = line.rstrip()
1373 1374 ui.note(line + '\n')
1374 1375 if line.startswith('patching file '):
1375 1376 pf = util.parsepatchoutput(line)
1376 1377 printed_file = False
1377 1378 files.add(pf)
1378 1379 elif line.find('with fuzz') >= 0:
1379 1380 fuzz = True
1380 1381 if not printed_file:
1381 1382 ui.warn(pf + '\n')
1382 1383 printed_file = True
1383 1384 ui.warn(line + '\n')
1384 1385 elif line.find('saving rejects to file') >= 0:
1385 1386 ui.warn(line + '\n')
1386 1387 elif line.find('FAILED') >= 0:
1387 1388 if not printed_file:
1388 1389 ui.warn(pf + '\n')
1389 1390 printed_file = True
1390 1391 ui.warn(line + '\n')
1391 1392 finally:
1392 1393 if files:
1393 1394 cfiles = list(files)
1394 1395 cwd = repo.getcwd()
1395 1396 if cwd:
1396 1397 cfiles = [util.pathto(repo.root, cwd, f)
1397 1398 for f in cfiles]
1398 1399 scmutil.addremove(repo, cfiles, similarity=similarity)
1399 1400 code = fp.close()
1400 1401 if code:
1401 1402 raise PatchError(_("patch command failed: %s") %
1402 1403 util.explainexit(code)[0])
1403 1404 return fuzz
1404 1405
1405 1406 def patchbackend(ui, backend, patchobj, strip, files=None, eolmode='strict'):
1406 1407 if files is None:
1407 1408 files = set()
1408 1409 if eolmode is None:
1409 1410 eolmode = ui.config('patch', 'eol', 'strict')
1410 1411 if eolmode.lower() not in eolmodes:
1411 1412 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1412 1413 eolmode = eolmode.lower()
1413 1414
1414 1415 store = filestore()
1415 1416 try:
1416 1417 fp = open(patchobj, 'rb')
1417 1418 except TypeError:
1418 1419 fp = patchobj
1419 1420 try:
1420 1421 ret = applydiff(ui, fp, backend, store, strip=strip,
1421 1422 eolmode=eolmode)
1422 1423 finally:
1423 1424 if fp != patchobj:
1424 1425 fp.close()
1425 1426 files.update(backend.close())
1426 1427 store.close()
1427 1428 if ret < 0:
1428 1429 raise PatchError(_('patch failed to apply'))
1429 1430 return ret > 0
1430 1431
1431 1432 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1432 1433 similarity=0):
1433 1434 """use builtin patch to apply <patchobj> to the working directory.
1434 1435 returns whether patch was applied with fuzz factor."""
1435 1436 backend = workingbackend(ui, repo, similarity)
1436 1437 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1437 1438
1438 1439 def patchrepo(ui, repo, ctx, store, patchobj, strip, files=None,
1439 1440 eolmode='strict'):
1440 1441 backend = repobackend(ui, repo, ctx, store)
1441 1442 return patchbackend(ui, backend, patchobj, strip, files, eolmode)
1442 1443
1443 1444 def makememctx(repo, parents, text, user, date, branch, files, store,
1444 1445 editor=None):
1445 1446 def getfilectx(repo, memctx, path):
1446 1447 data, (islink, isexec), copied = store.getfile(path)
1447 1448 return context.memfilectx(path, data, islink=islink, isexec=isexec,
1448 1449 copied=copied)
1449 1450 extra = {}
1450 1451 if branch:
1451 1452 extra['branch'] = encoding.fromlocal(branch)
1452 1453 ctx = context.memctx(repo, parents, text, files, getfilectx, user,
1453 1454 date, extra)
1454 1455 if editor:
1455 1456 ctx._text = editor(repo, ctx, [])
1456 1457 return ctx
1457 1458
1458 1459 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1459 1460 similarity=0):
1460 1461 """Apply <patchname> to the working directory.
1461 1462
1462 1463 'eolmode' specifies how end of lines should be handled. It can be:
1463 1464 - 'strict': inputs are read in binary mode, EOLs are preserved
1464 1465 - 'crlf': EOLs are ignored when patching and reset to CRLF
1465 1466 - 'lf': EOLs are ignored when patching and reset to LF
1466 1467 - None: get it from user settings, default to 'strict'
1467 1468 'eolmode' is ignored when using an external patcher program.
1468 1469
1469 1470 Returns whether patch was applied with fuzz factor.
1470 1471 """
1471 1472 patcher = ui.config('ui', 'patch')
1472 1473 if files is None:
1473 1474 files = set()
1474 1475 try:
1475 1476 if patcher:
1476 1477 return _externalpatch(ui, repo, patcher, patchname, strip,
1477 1478 files, similarity)
1478 1479 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1479 1480 similarity)
1480 1481 except PatchError, err:
1481 1482 raise util.Abort(str(err))
1482 1483
1483 1484 def changedfiles(ui, repo, patchpath, strip=1):
1484 1485 backend = fsbackend(ui, repo.root)
1485 1486 fp = open(patchpath, 'rb')
1486 1487 try:
1487 1488 changed = set()
1488 1489 for state, values in iterhunks(fp):
1489 1490 if state == 'file':
1490 1491 afile, bfile, first_hunk, gp = values
1491 1492 if gp:
1492 1493 gp.path = pathstrip(gp.path, strip - 1)[1]
1493 1494 if gp.oldpath:
1494 1495 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1495 1496 else:
1496 1497 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip)
1497 1498 changed.add(gp.path)
1498 1499 if gp.op == 'RENAME':
1499 1500 changed.add(gp.oldpath)
1500 1501 elif state not in ('hunk', 'git'):
1501 1502 raise util.Abort(_('unsupported parser state: %s') % state)
1502 1503 return changed
1503 1504 finally:
1504 1505 fp.close()
1505 1506
1506 1507 def b85diff(to, tn):
1507 1508 '''print base85-encoded binary diff'''
1508 1509 def gitindex(text):
1509 1510 if not text:
1510 1511 return hex(nullid)
1511 1512 l = len(text)
1512 1513 s = util.sha1('blob %d\0' % l)
1513 1514 s.update(text)
1514 1515 return s.hexdigest()
1515 1516
1516 1517 def fmtline(line):
1517 1518 l = len(line)
1518 1519 if l <= 26:
1519 1520 l = chr(ord('A') + l - 1)
1520 1521 else:
1521 1522 l = chr(l - 26 + ord('a') - 1)
1522 1523 return '%c%s\n' % (l, base85.b85encode(line, True))
1523 1524
1524 1525 def chunk(text, csize=52):
1525 1526 l = len(text)
1526 1527 i = 0
1527 1528 while i < l:
1528 1529 yield text[i:i + csize]
1529 1530 i += csize
1530 1531
1531 1532 tohash = gitindex(to)
1532 1533 tnhash = gitindex(tn)
1533 1534 if tohash == tnhash:
1534 1535 return ""
1535 1536
1536 1537 # TODO: deltas
1537 1538 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1538 1539 (tohash, tnhash, len(tn))]
1539 1540 for l in chunk(zlib.compress(tn)):
1540 1541 ret.append(fmtline(l))
1541 1542 ret.append('\n')
1542 1543 return ''.join(ret)
1543 1544
1544 1545 class GitDiffRequired(Exception):
1545 1546 pass
1546 1547
1547 1548 def diffopts(ui, opts=None, untrusted=False, section='diff'):
1548 1549 def get(key, name=None, getter=ui.configbool):
1549 1550 return ((opts and opts.get(key)) or
1550 1551 getter(section, name or key, None, untrusted=untrusted))
1551 1552 return mdiff.diffopts(
1552 1553 text=opts and opts.get('text'),
1553 1554 git=get('git'),
1554 1555 nodates=get('nodates'),
1555 1556 showfunc=get('show_function', 'showfunc'),
1556 1557 ignorews=get('ignore_all_space', 'ignorews'),
1557 1558 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1558 1559 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1559 1560 context=get('unified', getter=ui.config))
1560 1561
1561 1562 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1562 1563 losedatafn=None, prefix=''):
1563 1564 '''yields diff of changes to files between two nodes, or node and
1564 1565 working directory.
1565 1566
1566 1567 if node1 is None, use first dirstate parent instead.
1567 1568 if node2 is None, compare node1 with working directory.
1568 1569
1569 1570 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1570 1571 every time some change cannot be represented with the current
1571 1572 patch format. Return False to upgrade to git patch format, True to
1572 1573 accept the loss or raise an exception to abort the diff. It is
1573 1574 called with the name of current file being diffed as 'fn'. If set
1574 1575 to None, patches will always be upgraded to git format when
1575 1576 necessary.
1576 1577
1577 1578 prefix is a filename prefix that is prepended to all filenames on
1578 1579 display (used for subrepos).
1579 1580 '''
1580 1581
1581 1582 if opts is None:
1582 1583 opts = mdiff.defaultopts
1583 1584
1584 1585 if not node1 and not node2:
1585 1586 node1 = repo.dirstate.p1()
1586 1587
1587 1588 def lrugetfilectx():
1588 1589 cache = {}
1589 1590 order = []
1590 1591 def getfilectx(f, ctx):
1591 1592 fctx = ctx.filectx(f, filelog=cache.get(f))
1592 1593 if f not in cache:
1593 1594 if len(cache) > 20:
1594 1595 del cache[order.pop(0)]
1595 1596 cache[f] = fctx.filelog()
1596 1597 else:
1597 1598 order.remove(f)
1598 1599 order.append(f)
1599 1600 return fctx
1600 1601 return getfilectx
1601 1602 getfilectx = lrugetfilectx()
1602 1603
1603 1604 ctx1 = repo[node1]
1604 1605 ctx2 = repo[node2]
1605 1606
1606 1607 if not changes:
1607 1608 changes = repo.status(ctx1, ctx2, match=match)
1608 1609 modified, added, removed = changes[:3]
1609 1610
1610 1611 if not modified and not added and not removed:
1611 1612 return []
1612 1613
1613 1614 revs = None
1614 1615 if not repo.ui.quiet:
1615 1616 hexfunc = repo.ui.debugflag and hex or short
1616 1617 revs = [hexfunc(node) for node in [node1, node2] if node]
1617 1618
1618 1619 copy = {}
1619 1620 if opts.git or opts.upgrade:
1620 1621 copy = copies.pathcopies(ctx1, ctx2)
1621 1622
1622 1623 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1623 1624 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1624 1625 if opts.upgrade and not opts.git:
1625 1626 try:
1626 1627 def losedata(fn):
1627 1628 if not losedatafn or not losedatafn(fn=fn):
1628 1629 raise GitDiffRequired()
1629 1630 # Buffer the whole output until we are sure it can be generated
1630 1631 return list(difffn(opts.copy(git=False), losedata))
1631 1632 except GitDiffRequired:
1632 1633 return difffn(opts.copy(git=True), None)
1633 1634 else:
1634 1635 return difffn(opts, None)
1635 1636
1636 1637 def difflabel(func, *args, **kw):
1637 1638 '''yields 2-tuples of (output, label) based on the output of func()'''
1638 1639 headprefixes = [('diff', 'diff.diffline'),
1639 1640 ('copy', 'diff.extended'),
1640 1641 ('rename', 'diff.extended'),
1641 1642 ('old', 'diff.extended'),
1642 1643 ('new', 'diff.extended'),
1643 1644 ('deleted', 'diff.extended'),
1644 1645 ('---', 'diff.file_a'),
1645 1646 ('+++', 'diff.file_b')]
1646 1647 textprefixes = [('@', 'diff.hunk'),
1647 1648 ('-', 'diff.deleted'),
1648 1649 ('+', 'diff.inserted')]
1649 1650 head = False
1650 1651 for chunk in func(*args, **kw):
1651 1652 lines = chunk.split('\n')
1652 1653 for i, line in enumerate(lines):
1653 1654 if i != 0:
1654 1655 yield ('\n', '')
1655 1656 if head:
1656 1657 if line.startswith('@'):
1657 1658 head = False
1658 1659 else:
1659 1660 if line and not line[0] in ' +-@\\':
1660 1661 head = True
1661 1662 stripline = line
1662 1663 if not head and line and line[0] in '+-':
1663 1664 # highlight trailing whitespace, but only in changed lines
1664 1665 stripline = line.rstrip()
1665 1666 prefixes = textprefixes
1666 1667 if head:
1667 1668 prefixes = headprefixes
1668 1669 for prefix, label in prefixes:
1669 1670 if stripline.startswith(prefix):
1670 1671 yield (stripline, label)
1671 1672 break
1672 1673 else:
1673 1674 yield (line, '')
1674 1675 if line != stripline:
1675 1676 yield (line[len(stripline):], 'diff.trailingwhitespace')
1676 1677
1677 1678 def diffui(*args, **kw):
1678 1679 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1679 1680 return difflabel(diff, *args, **kw)
1680 1681
1681 1682
1682 1683 def _addmodehdr(header, omode, nmode):
1683 1684 if omode != nmode:
1684 1685 header.append('old mode %s\n' % omode)
1685 1686 header.append('new mode %s\n' % nmode)
1686 1687
1687 1688 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1688 1689 copy, getfilectx, opts, losedatafn, prefix):
1689 1690
1690 1691 def join(f):
1691 1692 return os.path.join(prefix, f)
1692 1693
1693 1694 date1 = util.datestr(ctx1.date())
1694 1695 man1 = ctx1.manifest()
1695 1696
1696 1697 gone = set()
1697 1698 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1698 1699
1699 1700 copyto = dict([(v, k) for k, v in copy.items()])
1700 1701
1701 1702 if opts.git:
1702 1703 revs = None
1703 1704
1704 1705 for f in sorted(modified + added + removed):
1705 1706 to = None
1706 1707 tn = None
1707 1708 dodiff = True
1708 1709 header = []
1709 1710 if f in man1:
1710 1711 to = getfilectx(f, ctx1).data()
1711 1712 if f not in removed:
1712 1713 tn = getfilectx(f, ctx2).data()
1713 1714 a, b = f, f
1714 1715 if opts.git or losedatafn:
1715 1716 if f in added:
1716 1717 mode = gitmode[ctx2.flags(f)]
1717 1718 if f in copy or f in copyto:
1718 1719 if opts.git:
1719 1720 if f in copy:
1720 1721 a = copy[f]
1721 1722 else:
1722 1723 a = copyto[f]
1723 1724 omode = gitmode[man1.flags(a)]
1724 1725 _addmodehdr(header, omode, mode)
1725 1726 if a in removed and a not in gone:
1726 1727 op = 'rename'
1727 1728 gone.add(a)
1728 1729 else:
1729 1730 op = 'copy'
1730 1731 header.append('%s from %s\n' % (op, join(a)))
1731 1732 header.append('%s to %s\n' % (op, join(f)))
1732 1733 to = getfilectx(a, ctx1).data()
1733 1734 else:
1734 1735 losedatafn(f)
1735 1736 else:
1736 1737 if opts.git:
1737 1738 header.append('new file mode %s\n' % mode)
1738 1739 elif ctx2.flags(f):
1739 1740 losedatafn(f)
1740 1741 # In theory, if tn was copied or renamed we should check
1741 1742 # if the source is binary too but the copy record already
1742 1743 # forces git mode.
1743 1744 if util.binary(tn):
1744 1745 if opts.git:
1745 1746 dodiff = 'binary'
1746 1747 else:
1747 1748 losedatafn(f)
1748 1749 if not opts.git and not tn:
1749 1750 # regular diffs cannot represent new empty file
1750 1751 losedatafn(f)
1751 1752 elif f in removed:
1752 1753 if opts.git:
1753 1754 # have we already reported a copy above?
1754 1755 if ((f in copy and copy[f] in added
1755 1756 and copyto[copy[f]] == f) or
1756 1757 (f in copyto and copyto[f] in added
1757 1758 and copy[copyto[f]] == f)):
1758 1759 dodiff = False
1759 1760 else:
1760 1761 header.append('deleted file mode %s\n' %
1761 1762 gitmode[man1.flags(f)])
1762 1763 elif not to or util.binary(to):
1763 1764 # regular diffs cannot represent empty file deletion
1764 1765 losedatafn(f)
1765 1766 else:
1766 1767 oflag = man1.flags(f)
1767 1768 nflag = ctx2.flags(f)
1768 1769 binary = util.binary(to) or util.binary(tn)
1769 1770 if opts.git:
1770 1771 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1771 1772 if binary:
1772 1773 dodiff = 'binary'
1773 1774 elif binary or nflag != oflag:
1774 1775 losedatafn(f)
1775 1776 if opts.git:
1776 1777 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1777 1778
1778 1779 if dodiff:
1779 1780 if dodiff == 'binary':
1780 1781 text = b85diff(to, tn)
1781 1782 else:
1782 1783 text = mdiff.unidiff(to, date1,
1783 1784 # ctx2 date may be dynamic
1784 1785 tn, util.datestr(ctx2.date()),
1785 1786 join(a), join(b), revs, opts=opts)
1786 1787 if header and (text or len(header) > 1):
1787 1788 yield ''.join(header)
1788 1789 if text:
1789 1790 yield text
1790 1791
1791 1792 def diffstatsum(stats):
1792 1793 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
1793 1794 for f, a, r, b in stats:
1794 1795 maxfile = max(maxfile, encoding.colwidth(f))
1795 1796 maxtotal = max(maxtotal, a + r)
1796 1797 addtotal += a
1797 1798 removetotal += r
1798 1799 binary = binary or b
1799 1800
1800 1801 return maxfile, maxtotal, addtotal, removetotal, binary
1801 1802
1802 1803 def diffstatdata(lines):
1803 1804 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1804 1805
1805 1806 results = []
1806 1807 filename, adds, removes, isbinary = None, 0, 0, False
1807 1808
1808 1809 def addresult():
1809 1810 if filename:
1810 1811 results.append((filename, adds, removes, isbinary))
1811 1812
1812 1813 for line in lines:
1813 1814 if line.startswith('diff'):
1814 1815 addresult()
1815 1816 # set numbers to 0 anyway when starting new file
1816 1817 adds, removes, isbinary = 0, 0, False
1817 1818 if line.startswith('diff --git'):
1818 1819 filename = gitre.search(line).group(1)
1819 1820 elif line.startswith('diff -r'):
1820 1821 # format: "diff -r ... -r ... filename"
1821 1822 filename = diffre.search(line).group(1)
1822 1823 elif line.startswith('+') and not line.startswith('+++ '):
1823 1824 adds += 1
1824 1825 elif line.startswith('-') and not line.startswith('--- '):
1825 1826 removes += 1
1826 1827 elif (line.startswith('GIT binary patch') or
1827 1828 line.startswith('Binary file')):
1828 1829 isbinary = True
1829 1830 addresult()
1830 1831 return results
1831 1832
1832 1833 def diffstat(lines, width=80, git=False):
1833 1834 output = []
1834 1835 stats = diffstatdata(lines)
1835 1836 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
1836 1837
1837 1838 countwidth = len(str(maxtotal))
1838 1839 if hasbinary and countwidth < 3:
1839 1840 countwidth = 3
1840 1841 graphwidth = width - countwidth - maxname - 6
1841 1842 if graphwidth < 10:
1842 1843 graphwidth = 10
1843 1844
1844 1845 def scale(i):
1845 1846 if maxtotal <= graphwidth:
1846 1847 return i
1847 1848 # If diffstat runs out of room it doesn't print anything,
1848 1849 # which isn't very useful, so always print at least one + or -
1849 1850 # if there were at least some changes.
1850 1851 return max(i * graphwidth // maxtotal, int(bool(i)))
1851 1852
1852 1853 for filename, adds, removes, isbinary in stats:
1853 1854 if isbinary:
1854 1855 count = 'Bin'
1855 1856 else:
1856 1857 count = adds + removes
1857 1858 pluses = '+' * scale(adds)
1858 1859 minuses = '-' * scale(removes)
1859 1860 output.append(' %s%s | %*s %s%s\n' %
1860 1861 (filename, ' ' * (maxname - encoding.colwidth(filename)),
1861 1862 countwidth, count, pluses, minuses))
1862 1863
1863 1864 if stats:
1864 1865 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1865 1866 % (len(stats), totaladds, totalremoves))
1866 1867
1867 1868 return ''.join(output)
1868 1869
1869 1870 def diffstatui(*args, **kw):
1870 1871 '''like diffstat(), but yields 2-tuples of (output, label) for
1871 1872 ui.write()
1872 1873 '''
1873 1874
1874 1875 for line in diffstat(*args, **kw).splitlines():
1875 1876 if line and line[-1] in '+-':
1876 1877 name, graph = line.rsplit(' ', 1)
1877 1878 yield (name + ' ', '')
1878 1879 m = re.search(r'\++', graph)
1879 1880 if m:
1880 1881 yield (m.group(0), 'diffstat.inserted')
1881 1882 m = re.search(r'-+', graph)
1882 1883 if m:
1883 1884 yield (m.group(0), 'diffstat.deleted')
1884 1885 else:
1885 1886 yield (line, '')
1886 1887 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now