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