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