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