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