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