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