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