##// END OF EJS Templates
patch: inline patchfile.hashlines()
Patrick Mezard -
r13700:63307feb default
parent child Browse files
Show More
@@ -1,1620 +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 def hashlines(self):
492 self.hash = {}
493 for x, s in enumerate(self.lines):
494 self.hash.setdefault(s, []).append(x)
495
496 491 def makerejlines(self, fname):
497 492 base = os.path.basename(fname)
498 493 yield "--- %s\n+++ %s\n" % (base, base)
499 494 for x in self.rej:
500 495 for l in x.hunk:
501 496 yield l
502 497 if l[-1] != '\n':
503 498 yield "\n\ No newline at end of file\n"
504 499
505 500 def write_rej(self):
506 501 # our rejects are a little different from patch(1). This always
507 502 # creates rejects in the same form as the original patch. A file
508 503 # header is inserted so that you can run the reject through patch again
509 504 # without having to type the filename.
510 505
511 506 if not self.rej:
512 507 return
513 508
514 509 fname = self.fname + ".rej"
515 510 self.ui.warn(
516 511 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
517 512 (len(self.rej), self.hunks, fname))
518 513
519 514 fp = self.opener(fname, 'w')
520 515 fp.writelines(self.makerejlines(self.fname))
521 516 fp.close()
522 517
523 518 def apply(self, h):
524 519 if not h.complete():
525 520 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
526 521 (h.number, h.desc, len(h.a), h.lena, len(h.b),
527 522 h.lenb))
528 523
529 524 self.hunks += 1
530 525
531 526 if self.missing:
532 527 self.rej.append(h)
533 528 return -1
534 529
535 530 if self.exists and h.createfile():
536 531 self.ui.warn(_("file %s already exists\n") % self.fname)
537 532 self.rej.append(h)
538 533 return -1
539 534
540 535 if isinstance(h, binhunk):
541 536 if h.rmfile():
542 537 self.unlink(self.fname)
543 538 else:
544 539 self.lines[:] = h.new()
545 540 self.offset += len(h.new())
546 541 self.dirty = 1
547 542 return 0
548 543
549 544 horig = h
550 545 if (self.eolmode in ('crlf', 'lf')
551 546 or self.eolmode == 'auto' and self.eol):
552 547 # If new eols are going to be normalized, then normalize
553 548 # hunk data before patching. Otherwise, preserve input
554 549 # line-endings.
555 550 h = h.getnormalized()
556 551
557 552 # fast case first, no offsets, no fuzz
558 553 old = h.old()
559 554 # patch starts counting at 1 unless we are adding the file
560 555 if h.starta == 0:
561 556 start = 0
562 557 else:
563 558 start = h.starta + self.offset - 1
564 559 orig_start = start
565 560 # if there's skew we want to emit the "(offset %d lines)" even
566 561 # when the hunk cleanly applies at start + skew, so skip the
567 562 # fast case code
568 563 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
569 564 if h.rmfile():
570 565 self.unlink(self.fname)
571 566 else:
572 567 self.lines[start : start + h.lena] = h.new()
573 568 self.offset += h.lenb - h.lena
574 569 self.dirty = 1
575 570 return 0
576 571
577 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
578 self.hashlines()
572 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
573 self.hash = {}
574 for x, s in enumerate(self.lines):
575 self.hash.setdefault(s, []).append(x)
579 576 if h.hunk[-1][0] != ' ':
580 577 # if the hunk tried to put something at the bottom of the file
581 578 # override the start line and use eof here
582 579 search_start = len(self.lines)
583 580 else:
584 581 search_start = orig_start + self.skew
585 582
586 583 for fuzzlen in xrange(3):
587 584 for toponly in [True, False]:
588 585 old = h.old(fuzzlen, toponly)
589 586
590 587 cand = self.findlines(old[0][1:], search_start)
591 588 for l in cand:
592 589 if diffhelpers.testhunk(old, self.lines, l) == 0:
593 590 newlines = h.new(fuzzlen, toponly)
594 591 self.lines[l : l + len(old)] = newlines
595 592 self.offset += len(newlines) - len(old)
596 593 self.skew = l - orig_start
597 594 self.dirty = 1
598 595 offset = l - orig_start - fuzzlen
599 596 if fuzzlen:
600 597 msg = _("Hunk #%d succeeded at %d "
601 598 "with fuzz %d "
602 599 "(offset %d lines).\n")
603 600 self.printfile(True)
604 601 self.ui.warn(msg %
605 602 (h.number, l + 1, fuzzlen, offset))
606 603 else:
607 604 msg = _("Hunk #%d succeeded at %d "
608 605 "(offset %d lines).\n")
609 606 self.ui.note(msg % (h.number, l + 1, offset))
610 607 return fuzzlen
611 608 self.printfile(True)
612 609 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
613 610 self.rej.append(horig)
614 611 return -1
615 612
616 613 class hunk(object):
617 614 def __init__(self, desc, num, lr, context, create=False, remove=False):
618 615 self.number = num
619 616 self.desc = desc
620 617 self.hunk = [desc]
621 618 self.a = []
622 619 self.b = []
623 620 self.starta = self.lena = None
624 621 self.startb = self.lenb = None
625 622 if lr is not None:
626 623 if context:
627 624 self.read_context_hunk(lr)
628 625 else:
629 626 self.read_unified_hunk(lr)
630 627 self.create = create
631 628 self.remove = remove and not create
632 629
633 630 def getnormalized(self):
634 631 """Return a copy with line endings normalized to LF."""
635 632
636 633 def normalize(lines):
637 634 nlines = []
638 635 for line in lines:
639 636 if line.endswith('\r\n'):
640 637 line = line[:-2] + '\n'
641 638 nlines.append(line)
642 639 return nlines
643 640
644 641 # Dummy object, it is rebuilt manually
645 642 nh = hunk(self.desc, self.number, None, None, False, False)
646 643 nh.number = self.number
647 644 nh.desc = self.desc
648 645 nh.hunk = self.hunk
649 646 nh.a = normalize(self.a)
650 647 nh.b = normalize(self.b)
651 648 nh.starta = self.starta
652 649 nh.startb = self.startb
653 650 nh.lena = self.lena
654 651 nh.lenb = self.lenb
655 652 nh.create = self.create
656 653 nh.remove = self.remove
657 654 return nh
658 655
659 656 def read_unified_hunk(self, lr):
660 657 m = unidesc.match(self.desc)
661 658 if not m:
662 659 raise PatchError(_("bad hunk #%d") % self.number)
663 660 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
664 661 if self.lena is None:
665 662 self.lena = 1
666 663 else:
667 664 self.lena = int(self.lena)
668 665 if self.lenb is None:
669 666 self.lenb = 1
670 667 else:
671 668 self.lenb = int(self.lenb)
672 669 self.starta = int(self.starta)
673 670 self.startb = int(self.startb)
674 671 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
675 672 # if we hit eof before finishing out the hunk, the last line will
676 673 # be zero length. Lets try to fix it up.
677 674 while len(self.hunk[-1]) == 0:
678 675 del self.hunk[-1]
679 676 del self.a[-1]
680 677 del self.b[-1]
681 678 self.lena -= 1
682 679 self.lenb -= 1
683 680 self._fixnewline(lr)
684 681
685 682 def read_context_hunk(self, lr):
686 683 self.desc = lr.readline()
687 684 m = contextdesc.match(self.desc)
688 685 if not m:
689 686 raise PatchError(_("bad hunk #%d") % self.number)
690 687 foo, self.starta, foo2, aend, foo3 = m.groups()
691 688 self.starta = int(self.starta)
692 689 if aend is None:
693 690 aend = self.starta
694 691 self.lena = int(aend) - self.starta
695 692 if self.starta:
696 693 self.lena += 1
697 694 for x in xrange(self.lena):
698 695 l = lr.readline()
699 696 if l.startswith('---'):
700 697 # lines addition, old block is empty
701 698 lr.push(l)
702 699 break
703 700 s = l[2:]
704 701 if l.startswith('- ') or l.startswith('! '):
705 702 u = '-' + s
706 703 elif l.startswith(' '):
707 704 u = ' ' + s
708 705 else:
709 706 raise PatchError(_("bad hunk #%d old text line %d") %
710 707 (self.number, x))
711 708 self.a.append(u)
712 709 self.hunk.append(u)
713 710
714 711 l = lr.readline()
715 712 if l.startswith('\ '):
716 713 s = self.a[-1][:-1]
717 714 self.a[-1] = s
718 715 self.hunk[-1] = s
719 716 l = lr.readline()
720 717 m = contextdesc.match(l)
721 718 if not m:
722 719 raise PatchError(_("bad hunk #%d") % self.number)
723 720 foo, self.startb, foo2, bend, foo3 = m.groups()
724 721 self.startb = int(self.startb)
725 722 if bend is None:
726 723 bend = self.startb
727 724 self.lenb = int(bend) - self.startb
728 725 if self.startb:
729 726 self.lenb += 1
730 727 hunki = 1
731 728 for x in xrange(self.lenb):
732 729 l = lr.readline()
733 730 if l.startswith('\ '):
734 731 # XXX: the only way to hit this is with an invalid line range.
735 732 # The no-eol marker is not counted in the line range, but I
736 733 # guess there are diff(1) out there which behave differently.
737 734 s = self.b[-1][:-1]
738 735 self.b[-1] = s
739 736 self.hunk[hunki - 1] = s
740 737 continue
741 738 if not l:
742 739 # line deletions, new block is empty and we hit EOF
743 740 lr.push(l)
744 741 break
745 742 s = l[2:]
746 743 if l.startswith('+ ') or l.startswith('! '):
747 744 u = '+' + s
748 745 elif l.startswith(' '):
749 746 u = ' ' + s
750 747 elif len(self.b) == 0:
751 748 # line deletions, new block is empty
752 749 lr.push(l)
753 750 break
754 751 else:
755 752 raise PatchError(_("bad hunk #%d old text line %d") %
756 753 (self.number, x))
757 754 self.b.append(s)
758 755 while True:
759 756 if hunki >= len(self.hunk):
760 757 h = ""
761 758 else:
762 759 h = self.hunk[hunki]
763 760 hunki += 1
764 761 if h == u:
765 762 break
766 763 elif h.startswith('-'):
767 764 continue
768 765 else:
769 766 self.hunk.insert(hunki - 1, u)
770 767 break
771 768
772 769 if not self.a:
773 770 # this happens when lines were only added to the hunk
774 771 for x in self.hunk:
775 772 if x.startswith('-') or x.startswith(' '):
776 773 self.a.append(x)
777 774 if not self.b:
778 775 # this happens when lines were only deleted from the hunk
779 776 for x in self.hunk:
780 777 if x.startswith('+') or x.startswith(' '):
781 778 self.b.append(x[1:])
782 779 # @@ -start,len +start,len @@
783 780 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
784 781 self.startb, self.lenb)
785 782 self.hunk[0] = self.desc
786 783 self._fixnewline(lr)
787 784
788 785 def _fixnewline(self, lr):
789 786 l = lr.readline()
790 787 if l.startswith('\ '):
791 788 diffhelpers.fix_newline(self.hunk, self.a, self.b)
792 789 else:
793 790 lr.push(l)
794 791
795 792 def complete(self):
796 793 return len(self.a) == self.lena and len(self.b) == self.lenb
797 794
798 795 def createfile(self):
799 796 return self.starta == 0 and self.lena == 0 and self.create
800 797
801 798 def rmfile(self):
802 799 return self.startb == 0 and self.lenb == 0 and self.remove
803 800
804 801 def fuzzit(self, l, fuzz, toponly):
805 802 # this removes context lines from the top and bottom of list 'l'. It
806 803 # checks the hunk to make sure only context lines are removed, and then
807 804 # returns a new shortened list of lines.
808 805 fuzz = min(fuzz, len(l)-1)
809 806 if fuzz:
810 807 top = 0
811 808 bot = 0
812 809 hlen = len(self.hunk)
813 810 for x in xrange(hlen - 1):
814 811 # the hunk starts with the @@ line, so use x+1
815 812 if self.hunk[x + 1][0] == ' ':
816 813 top += 1
817 814 else:
818 815 break
819 816 if not toponly:
820 817 for x in xrange(hlen - 1):
821 818 if self.hunk[hlen - bot - 1][0] == ' ':
822 819 bot += 1
823 820 else:
824 821 break
825 822
826 823 # top and bot now count context in the hunk
827 824 # adjust them if either one is short
828 825 context = max(top, bot, 3)
829 826 if bot < context:
830 827 bot = max(0, fuzz - (context - bot))
831 828 else:
832 829 bot = min(fuzz, bot)
833 830 if top < context:
834 831 top = max(0, fuzz - (context - top))
835 832 else:
836 833 top = min(fuzz, top)
837 834
838 835 return l[top:len(l)-bot]
839 836 return l
840 837
841 838 def old(self, fuzz=0, toponly=False):
842 839 return self.fuzzit(self.a, fuzz, toponly)
843 840
844 841 def new(self, fuzz=0, toponly=False):
845 842 return self.fuzzit(self.b, fuzz, toponly)
846 843
847 844 class binhunk:
848 845 'A binary patch file. Only understands literals so far.'
849 846 def __init__(self, gitpatch):
850 847 self.gitpatch = gitpatch
851 848 self.text = None
852 849 self.hunk = ['GIT binary patch\n']
853 850
854 851 def createfile(self):
855 852 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
856 853
857 854 def rmfile(self):
858 855 return self.gitpatch.op == 'DELETE'
859 856
860 857 def complete(self):
861 858 return self.text is not None
862 859
863 860 def new(self):
864 861 return [self.text]
865 862
866 863 def extract(self, lr):
867 864 line = lr.readline()
868 865 self.hunk.append(line)
869 866 while line and not line.startswith('literal '):
870 867 line = lr.readline()
871 868 self.hunk.append(line)
872 869 if not line:
873 870 raise PatchError(_('could not extract binary patch'))
874 871 size = int(line[8:].rstrip())
875 872 dec = []
876 873 line = lr.readline()
877 874 self.hunk.append(line)
878 875 while len(line) > 1:
879 876 l = line[0]
880 877 if l <= 'Z' and l >= 'A':
881 878 l = ord(l) - ord('A') + 1
882 879 else:
883 880 l = ord(l) - ord('a') + 27
884 881 dec.append(base85.b85decode(line[1:-1])[:l])
885 882 line = lr.readline()
886 883 self.hunk.append(line)
887 884 text = zlib.decompress(''.join(dec))
888 885 if len(text) != size:
889 886 raise PatchError(_('binary patch is %d bytes, not %d') %
890 887 len(text), size)
891 888 self.text = text
892 889
893 890 def parsefilename(str):
894 891 # --- filename \t|space stuff
895 892 s = str[4:].rstrip('\r\n')
896 893 i = s.find('\t')
897 894 if i < 0:
898 895 i = s.find(' ')
899 896 if i < 0:
900 897 return s
901 898 return s[:i]
902 899
903 900 def pathstrip(path, strip):
904 901 pathlen = len(path)
905 902 i = 0
906 903 if strip == 0:
907 904 return '', path.rstrip()
908 905 count = strip
909 906 while count > 0:
910 907 i = path.find('/', i)
911 908 if i == -1:
912 909 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
913 910 (count, strip, path))
914 911 i += 1
915 912 # consume '//' in the path
916 913 while i < pathlen - 1 and path[i] == '/':
917 914 i += 1
918 915 count -= 1
919 916 return path[:i].lstrip(), path[i:].rstrip()
920 917
921 918 def selectfile(afile_orig, bfile_orig, hunk, strip):
922 919 nulla = afile_orig == "/dev/null"
923 920 nullb = bfile_orig == "/dev/null"
924 921 abase, afile = pathstrip(afile_orig, strip)
925 922 gooda = not nulla and os.path.lexists(afile)
926 923 bbase, bfile = pathstrip(bfile_orig, strip)
927 924 if afile == bfile:
928 925 goodb = gooda
929 926 else:
930 927 goodb = not nullb and os.path.lexists(bfile)
931 928 createfunc = hunk.createfile
932 929 missing = not goodb and not gooda and not createfunc()
933 930
934 931 # some diff programs apparently produce patches where the afile is
935 932 # not /dev/null, but afile starts with bfile
936 933 abasedir = afile[:afile.rfind('/') + 1]
937 934 bbasedir = bfile[:bfile.rfind('/') + 1]
938 935 if missing and abasedir == bbasedir and afile.startswith(bfile):
939 936 # this isn't very pretty
940 937 hunk.create = True
941 938 if createfunc():
942 939 missing = False
943 940 else:
944 941 hunk.create = False
945 942
946 943 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
947 944 # diff is between a file and its backup. In this case, the original
948 945 # file should be patched (see original mpatch code).
949 946 isbackup = (abase == bbase and bfile.startswith(afile))
950 947 fname = None
951 948 if not missing:
952 949 if gooda and goodb:
953 950 fname = isbackup and afile or bfile
954 951 elif gooda:
955 952 fname = afile
956 953
957 954 if not fname:
958 955 if not nullb:
959 956 fname = isbackup and afile or bfile
960 957 elif not nulla:
961 958 fname = afile
962 959 else:
963 960 raise PatchError(_("undefined source and destination files"))
964 961
965 962 return fname, missing
966 963
967 964 def scangitpatch(lr, firstline):
968 965 """
969 966 Git patches can emit:
970 967 - rename a to b
971 968 - change b
972 969 - copy a to c
973 970 - change c
974 971
975 972 We cannot apply this sequence as-is, the renamed 'a' could not be
976 973 found for it would have been renamed already. And we cannot copy
977 974 from 'b' instead because 'b' would have been changed already. So
978 975 we scan the git patch for copy and rename commands so we can
979 976 perform the copies ahead of time.
980 977 """
981 978 pos = 0
982 979 try:
983 980 pos = lr.fp.tell()
984 981 fp = lr.fp
985 982 except IOError:
986 983 fp = cStringIO.StringIO(lr.fp.read())
987 984 gitlr = linereader(fp, lr.textmode)
988 985 gitlr.push(firstline)
989 986 gitpatches = readgitpatch(gitlr)
990 987 fp.seek(pos)
991 988 return gitpatches
992 989
993 990 def iterhunks(ui, fp):
994 991 """Read a patch and yield the following events:
995 992 - ("file", afile, bfile, firsthunk): select a new target file.
996 993 - ("hunk", hunk): a new hunk is ready to be applied, follows a
997 994 "file" event.
998 995 - ("git", gitchanges): current diff is in git format, gitchanges
999 996 maps filenames to gitpatch records. Unique event.
1000 997 """
1001 998 changed = {}
1002 999 afile = ""
1003 1000 bfile = ""
1004 1001 state = None
1005 1002 hunknum = 0
1006 1003 emitfile = False
1007 1004 git = False
1008 1005
1009 1006 # our states
1010 1007 BFILE = 1
1011 1008 context = None
1012 1009 lr = linereader(fp)
1013 1010
1014 1011 while True:
1015 1012 newfile = newgitfile = False
1016 1013 x = lr.readline()
1017 1014 if not x:
1018 1015 break
1019 1016 if (state == BFILE and ((not context and x[0] == '@') or
1020 1017 ((context is not False) and x.startswith('***************')))):
1021 1018 if context is None and x.startswith('***************'):
1022 1019 context = True
1023 1020 gpatch = changed.get(bfile)
1024 1021 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1025 1022 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1026 1023 h = hunk(x, hunknum + 1, lr, context, create, remove)
1027 1024 hunknum += 1
1028 1025 if emitfile:
1029 1026 emitfile = False
1030 1027 yield 'file', (afile, bfile, h)
1031 1028 yield 'hunk', h
1032 1029 elif state == BFILE and x.startswith('GIT binary patch'):
1033 1030 h = binhunk(changed[bfile])
1034 1031 hunknum += 1
1035 1032 if emitfile:
1036 1033 emitfile = False
1037 1034 yield 'file', ('a/' + afile, 'b/' + bfile, h)
1038 1035 h.extract(lr)
1039 1036 yield 'hunk', h
1040 1037 elif x.startswith('diff --git'):
1041 1038 # check for git diff, scanning the whole patch file if needed
1042 1039 m = gitre.match(x)
1043 1040 if m:
1044 1041 afile, bfile = m.group(1, 2)
1045 1042 if not git:
1046 1043 git = True
1047 1044 gitpatches = scangitpatch(lr, x)
1048 1045 yield 'git', gitpatches
1049 1046 for gp in gitpatches:
1050 1047 changed[gp.path] = gp
1051 1048 # else error?
1052 1049 # copy/rename + modify should modify target, not source
1053 1050 gp = changed.get(bfile)
1054 1051 if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
1055 1052 or gp.mode):
1056 1053 afile = bfile
1057 1054 newgitfile = True
1058 1055 elif x.startswith('---'):
1059 1056 # check for a unified diff
1060 1057 l2 = lr.readline()
1061 1058 if not l2.startswith('+++'):
1062 1059 lr.push(l2)
1063 1060 continue
1064 1061 newfile = True
1065 1062 context = False
1066 1063 afile = parsefilename(x)
1067 1064 bfile = parsefilename(l2)
1068 1065 elif x.startswith('***'):
1069 1066 # check for a context diff
1070 1067 l2 = lr.readline()
1071 1068 if not l2.startswith('---'):
1072 1069 lr.push(l2)
1073 1070 continue
1074 1071 l3 = lr.readline()
1075 1072 lr.push(l3)
1076 1073 if not l3.startswith("***************"):
1077 1074 lr.push(l2)
1078 1075 continue
1079 1076 newfile = True
1080 1077 context = True
1081 1078 afile = parsefilename(x)
1082 1079 bfile = parsefilename(l2)
1083 1080
1084 1081 if newgitfile or newfile:
1085 1082 emitfile = True
1086 1083 state = BFILE
1087 1084 hunknum = 0
1088 1085
1089 1086 def applydiff(ui, fp, changed, strip=1, eolmode='strict'):
1090 1087 """Reads a patch from fp and tries to apply it.
1091 1088
1092 1089 The dict 'changed' is filled in with all of the filenames changed
1093 1090 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1094 1091 found and 1 if there was any fuzz.
1095 1092
1096 1093 If 'eolmode' is 'strict', the patch content and patched file are
1097 1094 read in binary mode. Otherwise, line endings are ignored when
1098 1095 patching then normalized according to 'eolmode'.
1099 1096
1100 1097 Callers probably want to call 'cmdutil.updatedir' after this to
1101 1098 apply certain categories of changes not done by this function.
1102 1099 """
1103 1100 return _applydiff(ui, fp, patchfile, copyfile, changed, strip=strip,
1104 1101 eolmode=eolmode)
1105 1102
1106 1103 def _applydiff(ui, fp, patcher, copyfn, changed, strip=1, eolmode='strict'):
1107 1104 rejects = 0
1108 1105 err = 0
1109 1106 current_file = None
1110 1107 cwd = os.getcwd()
1111 1108 opener = util.opener(cwd)
1112 1109
1113 1110 def closefile():
1114 1111 if not current_file:
1115 1112 return 0
1116 1113 if current_file.dirty:
1117 1114 current_file.writelines(current_file.fname, current_file.lines)
1118 1115 current_file.write_rej()
1119 1116 return len(current_file.rej)
1120 1117
1121 1118 for state, values in iterhunks(ui, fp):
1122 1119 if state == 'hunk':
1123 1120 if not current_file:
1124 1121 continue
1125 1122 ret = current_file.apply(values)
1126 1123 if ret >= 0:
1127 1124 changed.setdefault(current_file.fname, None)
1128 1125 if ret > 0:
1129 1126 err = 1
1130 1127 elif state == 'file':
1131 1128 rejects += closefile()
1132 1129 afile, bfile, first_hunk = values
1133 1130 try:
1134 1131 current_file, missing = selectfile(afile, bfile,
1135 1132 first_hunk, strip)
1136 1133 current_file = patcher(ui, current_file, opener,
1137 1134 missing=missing, eolmode=eolmode)
1138 1135 except PatchError, err:
1139 1136 ui.warn(str(err) + '\n')
1140 1137 current_file = None
1141 1138 rejects += 1
1142 1139 continue
1143 1140 elif state == 'git':
1144 1141 for gp in values:
1145 1142 gp.path = pathstrip(gp.path, strip - 1)[1]
1146 1143 if gp.oldpath:
1147 1144 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1148 1145 # Binary patches really overwrite target files, copying them
1149 1146 # will just make it fails with "target file exists"
1150 1147 if gp.op in ('COPY', 'RENAME') and not gp.binary:
1151 1148 copyfn(gp.oldpath, gp.path, cwd)
1152 1149 changed[gp.path] = gp
1153 1150 else:
1154 1151 raise util.Abort(_('unsupported parser state: %s') % state)
1155 1152
1156 1153 rejects += closefile()
1157 1154
1158 1155 if rejects:
1159 1156 return -1
1160 1157 return err
1161 1158
1162 1159 def externalpatch(patcher, patchname, ui, strip, cwd, files):
1163 1160 """use <patcher> to apply <patchname> to the working directory.
1164 1161 returns whether patch was applied with fuzz factor."""
1165 1162
1166 1163 fuzz = False
1167 1164 args = []
1168 1165 if cwd:
1169 1166 args.append('-d %s' % util.shellquote(cwd))
1170 1167 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1171 1168 util.shellquote(patchname)))
1172 1169
1173 1170 for line in fp:
1174 1171 line = line.rstrip()
1175 1172 ui.note(line + '\n')
1176 1173 if line.startswith('patching file '):
1177 1174 pf = util.parse_patch_output(line)
1178 1175 printed_file = False
1179 1176 files.setdefault(pf, None)
1180 1177 elif line.find('with fuzz') >= 0:
1181 1178 fuzz = True
1182 1179 if not printed_file:
1183 1180 ui.warn(pf + '\n')
1184 1181 printed_file = True
1185 1182 ui.warn(line + '\n')
1186 1183 elif line.find('saving rejects to file') >= 0:
1187 1184 ui.warn(line + '\n')
1188 1185 elif line.find('FAILED') >= 0:
1189 1186 if not printed_file:
1190 1187 ui.warn(pf + '\n')
1191 1188 printed_file = True
1192 1189 ui.warn(line + '\n')
1193 1190 code = fp.close()
1194 1191 if code:
1195 1192 raise PatchError(_("patch command failed: %s") %
1196 1193 util.explain_exit(code)[0])
1197 1194 return fuzz
1198 1195
1199 1196 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1200 1197 """use builtin patch to apply <patchobj> to the working directory.
1201 1198 returns whether patch was applied with fuzz factor."""
1202 1199
1203 1200 if files is None:
1204 1201 files = {}
1205 1202 if eolmode is None:
1206 1203 eolmode = ui.config('patch', 'eol', 'strict')
1207 1204 if eolmode.lower() not in eolmodes:
1208 1205 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1209 1206 eolmode = eolmode.lower()
1210 1207
1211 1208 try:
1212 1209 fp = open(patchobj, 'rb')
1213 1210 except TypeError:
1214 1211 fp = patchobj
1215 1212 if cwd:
1216 1213 curdir = os.getcwd()
1217 1214 os.chdir(cwd)
1218 1215 try:
1219 1216 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1220 1217 finally:
1221 1218 if cwd:
1222 1219 os.chdir(curdir)
1223 1220 if fp != patchobj:
1224 1221 fp.close()
1225 1222 if ret < 0:
1226 1223 raise PatchError(_('patch failed to apply'))
1227 1224 return ret > 0
1228 1225
1229 1226 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1230 1227 """Apply <patchname> to the working directory.
1231 1228
1232 1229 'eolmode' specifies how end of lines should be handled. It can be:
1233 1230 - 'strict': inputs are read in binary mode, EOLs are preserved
1234 1231 - 'crlf': EOLs are ignored when patching and reset to CRLF
1235 1232 - 'lf': EOLs are ignored when patching and reset to LF
1236 1233 - None: get it from user settings, default to 'strict'
1237 1234 'eolmode' is ignored when using an external patcher program.
1238 1235
1239 1236 Returns whether patch was applied with fuzz factor.
1240 1237 """
1241 1238 patcher = ui.config('ui', 'patch')
1242 1239 if files is None:
1243 1240 files = {}
1244 1241 try:
1245 1242 if patcher:
1246 1243 return externalpatch(patcher, patchname, ui, strip, cwd, files)
1247 1244 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1248 1245 except PatchError, err:
1249 1246 raise util.Abort(str(err))
1250 1247
1251 1248 def b85diff(to, tn):
1252 1249 '''print base85-encoded binary diff'''
1253 1250 def gitindex(text):
1254 1251 if not text:
1255 1252 return hex(nullid)
1256 1253 l = len(text)
1257 1254 s = util.sha1('blob %d\0' % l)
1258 1255 s.update(text)
1259 1256 return s.hexdigest()
1260 1257
1261 1258 def fmtline(line):
1262 1259 l = len(line)
1263 1260 if l <= 26:
1264 1261 l = chr(ord('A') + l - 1)
1265 1262 else:
1266 1263 l = chr(l - 26 + ord('a') - 1)
1267 1264 return '%c%s\n' % (l, base85.b85encode(line, True))
1268 1265
1269 1266 def chunk(text, csize=52):
1270 1267 l = len(text)
1271 1268 i = 0
1272 1269 while i < l:
1273 1270 yield text[i:i + csize]
1274 1271 i += csize
1275 1272
1276 1273 tohash = gitindex(to)
1277 1274 tnhash = gitindex(tn)
1278 1275 if tohash == tnhash:
1279 1276 return ""
1280 1277
1281 1278 # TODO: deltas
1282 1279 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1283 1280 (tohash, tnhash, len(tn))]
1284 1281 for l in chunk(zlib.compress(tn)):
1285 1282 ret.append(fmtline(l))
1286 1283 ret.append('\n')
1287 1284 return ''.join(ret)
1288 1285
1289 1286 class GitDiffRequired(Exception):
1290 1287 pass
1291 1288
1292 1289 def diffopts(ui, opts=None, untrusted=False):
1293 1290 def get(key, name=None, getter=ui.configbool):
1294 1291 return ((opts and opts.get(key)) or
1295 1292 getter('diff', name or key, None, untrusted=untrusted))
1296 1293 return mdiff.diffopts(
1297 1294 text=opts and opts.get('text'),
1298 1295 git=get('git'),
1299 1296 nodates=get('nodates'),
1300 1297 showfunc=get('show_function', 'showfunc'),
1301 1298 ignorews=get('ignore_all_space', 'ignorews'),
1302 1299 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1303 1300 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1304 1301 context=get('unified', getter=ui.config))
1305 1302
1306 1303 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1307 1304 losedatafn=None, prefix=''):
1308 1305 '''yields diff of changes to files between two nodes, or node and
1309 1306 working directory.
1310 1307
1311 1308 if node1 is None, use first dirstate parent instead.
1312 1309 if node2 is None, compare node1 with working directory.
1313 1310
1314 1311 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1315 1312 every time some change cannot be represented with the current
1316 1313 patch format. Return False to upgrade to git patch format, True to
1317 1314 accept the loss or raise an exception to abort the diff. It is
1318 1315 called with the name of current file being diffed as 'fn'. If set
1319 1316 to None, patches will always be upgraded to git format when
1320 1317 necessary.
1321 1318
1322 1319 prefix is a filename prefix that is prepended to all filenames on
1323 1320 display (used for subrepos).
1324 1321 '''
1325 1322
1326 1323 if opts is None:
1327 1324 opts = mdiff.defaultopts
1328 1325
1329 1326 if not node1 and not node2:
1330 1327 node1 = repo.dirstate.parents()[0]
1331 1328
1332 1329 def lrugetfilectx():
1333 1330 cache = {}
1334 1331 order = []
1335 1332 def getfilectx(f, ctx):
1336 1333 fctx = ctx.filectx(f, filelog=cache.get(f))
1337 1334 if f not in cache:
1338 1335 if len(cache) > 20:
1339 1336 del cache[order.pop(0)]
1340 1337 cache[f] = fctx.filelog()
1341 1338 else:
1342 1339 order.remove(f)
1343 1340 order.append(f)
1344 1341 return fctx
1345 1342 return getfilectx
1346 1343 getfilectx = lrugetfilectx()
1347 1344
1348 1345 ctx1 = repo[node1]
1349 1346 ctx2 = repo[node2]
1350 1347
1351 1348 if not changes:
1352 1349 changes = repo.status(ctx1, ctx2, match=match)
1353 1350 modified, added, removed = changes[:3]
1354 1351
1355 1352 if not modified and not added and not removed:
1356 1353 return []
1357 1354
1358 1355 revs = None
1359 1356 if not repo.ui.quiet:
1360 1357 hexfunc = repo.ui.debugflag and hex or short
1361 1358 revs = [hexfunc(node) for node in [node1, node2] if node]
1362 1359
1363 1360 copy = {}
1364 1361 if opts.git or opts.upgrade:
1365 1362 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1366 1363
1367 1364 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1368 1365 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1369 1366 if opts.upgrade and not opts.git:
1370 1367 try:
1371 1368 def losedata(fn):
1372 1369 if not losedatafn or not losedatafn(fn=fn):
1373 1370 raise GitDiffRequired()
1374 1371 # Buffer the whole output until we are sure it can be generated
1375 1372 return list(difffn(opts.copy(git=False), losedata))
1376 1373 except GitDiffRequired:
1377 1374 return difffn(opts.copy(git=True), None)
1378 1375 else:
1379 1376 return difffn(opts, None)
1380 1377
1381 1378 def difflabel(func, *args, **kw):
1382 1379 '''yields 2-tuples of (output, label) based on the output of func()'''
1383 1380 prefixes = [('diff', 'diff.diffline'),
1384 1381 ('copy', 'diff.extended'),
1385 1382 ('rename', 'diff.extended'),
1386 1383 ('old', 'diff.extended'),
1387 1384 ('new', 'diff.extended'),
1388 1385 ('deleted', 'diff.extended'),
1389 1386 ('---', 'diff.file_a'),
1390 1387 ('+++', 'diff.file_b'),
1391 1388 ('@@', 'diff.hunk'),
1392 1389 ('-', 'diff.deleted'),
1393 1390 ('+', 'diff.inserted')]
1394 1391
1395 1392 for chunk in func(*args, **kw):
1396 1393 lines = chunk.split('\n')
1397 1394 for i, line in enumerate(lines):
1398 1395 if i != 0:
1399 1396 yield ('\n', '')
1400 1397 stripline = line
1401 1398 if line and line[0] in '+-':
1402 1399 # highlight trailing whitespace, but only in changed lines
1403 1400 stripline = line.rstrip()
1404 1401 for prefix, label in prefixes:
1405 1402 if stripline.startswith(prefix):
1406 1403 yield (stripline, label)
1407 1404 break
1408 1405 else:
1409 1406 yield (line, '')
1410 1407 if line != stripline:
1411 1408 yield (line[len(stripline):], 'diff.trailingwhitespace')
1412 1409
1413 1410 def diffui(*args, **kw):
1414 1411 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1415 1412 return difflabel(diff, *args, **kw)
1416 1413
1417 1414
1418 1415 def _addmodehdr(header, omode, nmode):
1419 1416 if omode != nmode:
1420 1417 header.append('old mode %s\n' % omode)
1421 1418 header.append('new mode %s\n' % nmode)
1422 1419
1423 1420 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1424 1421 copy, getfilectx, opts, losedatafn, prefix):
1425 1422
1426 1423 def join(f):
1427 1424 return os.path.join(prefix, f)
1428 1425
1429 1426 date1 = util.datestr(ctx1.date())
1430 1427 man1 = ctx1.manifest()
1431 1428
1432 1429 gone = set()
1433 1430 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1434 1431
1435 1432 copyto = dict([(v, k) for k, v in copy.items()])
1436 1433
1437 1434 if opts.git:
1438 1435 revs = None
1439 1436
1440 1437 for f in sorted(modified + added + removed):
1441 1438 to = None
1442 1439 tn = None
1443 1440 dodiff = True
1444 1441 header = []
1445 1442 if f in man1:
1446 1443 to = getfilectx(f, ctx1).data()
1447 1444 if f not in removed:
1448 1445 tn = getfilectx(f, ctx2).data()
1449 1446 a, b = f, f
1450 1447 if opts.git or losedatafn:
1451 1448 if f in added:
1452 1449 mode = gitmode[ctx2.flags(f)]
1453 1450 if f in copy or f in copyto:
1454 1451 if opts.git:
1455 1452 if f in copy:
1456 1453 a = copy[f]
1457 1454 else:
1458 1455 a = copyto[f]
1459 1456 omode = gitmode[man1.flags(a)]
1460 1457 _addmodehdr(header, omode, mode)
1461 1458 if a in removed and a not in gone:
1462 1459 op = 'rename'
1463 1460 gone.add(a)
1464 1461 else:
1465 1462 op = 'copy'
1466 1463 header.append('%s from %s\n' % (op, join(a)))
1467 1464 header.append('%s to %s\n' % (op, join(f)))
1468 1465 to = getfilectx(a, ctx1).data()
1469 1466 else:
1470 1467 losedatafn(f)
1471 1468 else:
1472 1469 if opts.git:
1473 1470 header.append('new file mode %s\n' % mode)
1474 1471 elif ctx2.flags(f):
1475 1472 losedatafn(f)
1476 1473 # In theory, if tn was copied or renamed we should check
1477 1474 # if the source is binary too but the copy record already
1478 1475 # forces git mode.
1479 1476 if util.binary(tn):
1480 1477 if opts.git:
1481 1478 dodiff = 'binary'
1482 1479 else:
1483 1480 losedatafn(f)
1484 1481 if not opts.git and not tn:
1485 1482 # regular diffs cannot represent new empty file
1486 1483 losedatafn(f)
1487 1484 elif f in removed:
1488 1485 if opts.git:
1489 1486 # have we already reported a copy above?
1490 1487 if ((f in copy and copy[f] in added
1491 1488 and copyto[copy[f]] == f) or
1492 1489 (f in copyto and copyto[f] in added
1493 1490 and copy[copyto[f]] == f)):
1494 1491 dodiff = False
1495 1492 else:
1496 1493 header.append('deleted file mode %s\n' %
1497 1494 gitmode[man1.flags(f)])
1498 1495 elif not to or util.binary(to):
1499 1496 # regular diffs cannot represent empty file deletion
1500 1497 losedatafn(f)
1501 1498 else:
1502 1499 oflag = man1.flags(f)
1503 1500 nflag = ctx2.flags(f)
1504 1501 binary = util.binary(to) or util.binary(tn)
1505 1502 if opts.git:
1506 1503 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1507 1504 if binary:
1508 1505 dodiff = 'binary'
1509 1506 elif binary or nflag != oflag:
1510 1507 losedatafn(f)
1511 1508 if opts.git:
1512 1509 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1513 1510
1514 1511 if dodiff:
1515 1512 if dodiff == 'binary':
1516 1513 text = b85diff(to, tn)
1517 1514 else:
1518 1515 text = mdiff.unidiff(to, date1,
1519 1516 # ctx2 date may be dynamic
1520 1517 tn, util.datestr(ctx2.date()),
1521 1518 join(a), join(b), revs, opts=opts)
1522 1519 if header and (text or len(header) > 1):
1523 1520 yield ''.join(header)
1524 1521 if text:
1525 1522 yield text
1526 1523
1527 1524 def diffstatdata(lines):
1528 1525 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1529 1526
1530 1527 filename, adds, removes = None, 0, 0
1531 1528 for line in lines:
1532 1529 if line.startswith('diff'):
1533 1530 if filename:
1534 1531 isbinary = adds == 0 and removes == 0
1535 1532 yield (filename, adds, removes, isbinary)
1536 1533 # set numbers to 0 anyway when starting new file
1537 1534 adds, removes = 0, 0
1538 1535 if line.startswith('diff --git'):
1539 1536 filename = gitre.search(line).group(1)
1540 1537 elif line.startswith('diff -r'):
1541 1538 # format: "diff -r ... -r ... filename"
1542 1539 filename = diffre.search(line).group(1)
1543 1540 elif line.startswith('+') and not line.startswith('+++'):
1544 1541 adds += 1
1545 1542 elif line.startswith('-') and not line.startswith('---'):
1546 1543 removes += 1
1547 1544 if filename:
1548 1545 isbinary = adds == 0 and removes == 0
1549 1546 yield (filename, adds, removes, isbinary)
1550 1547
1551 1548 def diffstat(lines, width=80, git=False):
1552 1549 output = []
1553 1550 stats = list(diffstatdata(lines))
1554 1551
1555 1552 maxtotal, maxname = 0, 0
1556 1553 totaladds, totalremoves = 0, 0
1557 1554 hasbinary = False
1558 1555
1559 1556 sized = [(filename, adds, removes, isbinary, encoding.colwidth(filename))
1560 1557 for filename, adds, removes, isbinary in stats]
1561 1558
1562 1559 for filename, adds, removes, isbinary, namewidth in sized:
1563 1560 totaladds += adds
1564 1561 totalremoves += removes
1565 1562 maxname = max(maxname, namewidth)
1566 1563 maxtotal = max(maxtotal, adds + removes)
1567 1564 if isbinary:
1568 1565 hasbinary = True
1569 1566
1570 1567 countwidth = len(str(maxtotal))
1571 1568 if hasbinary and countwidth < 3:
1572 1569 countwidth = 3
1573 1570 graphwidth = width - countwidth - maxname - 6
1574 1571 if graphwidth < 10:
1575 1572 graphwidth = 10
1576 1573
1577 1574 def scale(i):
1578 1575 if maxtotal <= graphwidth:
1579 1576 return i
1580 1577 # If diffstat runs out of room it doesn't print anything,
1581 1578 # which isn't very useful, so always print at least one + or -
1582 1579 # if there were at least some changes.
1583 1580 return max(i * graphwidth // maxtotal, int(bool(i)))
1584 1581
1585 1582 for filename, adds, removes, isbinary, namewidth in sized:
1586 1583 if git and isbinary:
1587 1584 count = 'Bin'
1588 1585 else:
1589 1586 count = adds + removes
1590 1587 pluses = '+' * scale(adds)
1591 1588 minuses = '-' * scale(removes)
1592 1589 output.append(' %s%s | %*s %s%s\n' %
1593 1590 (filename, ' ' * (maxname - namewidth),
1594 1591 countwidth, count,
1595 1592 pluses, minuses))
1596 1593
1597 1594 if stats:
1598 1595 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1599 1596 % (len(stats), totaladds, totalremoves))
1600 1597
1601 1598 return ''.join(output)
1602 1599
1603 1600 def diffstatui(*args, **kw):
1604 1601 '''like diffstat(), but yields 2-tuples of (output, label) for
1605 1602 ui.write()
1606 1603 '''
1607 1604
1608 1605 for line in diffstat(*args, **kw).splitlines():
1609 1606 if line and line[-1] in '+-':
1610 1607 name, graph = line.rsplit(' ', 1)
1611 1608 yield (name + ' ', '')
1612 1609 m = re.search(r'\++', graph)
1613 1610 if m:
1614 1611 yield (m.group(0), 'diffstat.inserted')
1615 1612 m = re.search(r'-+', graph)
1616 1613 if m:
1617 1614 yield (m.group(0), 'diffstat.deleted')
1618 1615 else:
1619 1616 yield (line, '')
1620 1617 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now