##// END OF EJS Templates
patch: introduce changedfiles...
Idan Kamara -
r14255:576256a8 default
parent child Browse files
Show More
@@ -1,1618 +1,1640 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 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 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 def changedfiles(patchpath, strip=1):
1250 fp = open(patchpath, 'rb')
1251 try:
1252 changed = set()
1253 for state, values in iterhunks(fp):
1254 if state == 'hunk':
1255 continue
1256 elif state == 'file':
1257 afile, bfile, first_hunk = values
1258 current_file, missing = selectfile(afile, bfile,
1259 first_hunk, strip)
1260 changed.add(current_file)
1261 elif state == 'git':
1262 for gp in values:
1263 gp.path = pathstrip(gp.path, strip - 1)[1]
1264 changed.add(gp.path)
1265 else:
1266 raise util.Abort(_('unsupported parser state: %s') % state)
1267 return changed
1268 finally:
1269 fp.close()
1270
1249 1271 def b85diff(to, tn):
1250 1272 '''print base85-encoded binary diff'''
1251 1273 def gitindex(text):
1252 1274 if not text:
1253 1275 return hex(nullid)
1254 1276 l = len(text)
1255 1277 s = util.sha1('blob %d\0' % l)
1256 1278 s.update(text)
1257 1279 return s.hexdigest()
1258 1280
1259 1281 def fmtline(line):
1260 1282 l = len(line)
1261 1283 if l <= 26:
1262 1284 l = chr(ord('A') + l - 1)
1263 1285 else:
1264 1286 l = chr(l - 26 + ord('a') - 1)
1265 1287 return '%c%s\n' % (l, base85.b85encode(line, True))
1266 1288
1267 1289 def chunk(text, csize=52):
1268 1290 l = len(text)
1269 1291 i = 0
1270 1292 while i < l:
1271 1293 yield text[i:i + csize]
1272 1294 i += csize
1273 1295
1274 1296 tohash = gitindex(to)
1275 1297 tnhash = gitindex(tn)
1276 1298 if tohash == tnhash:
1277 1299 return ""
1278 1300
1279 1301 # TODO: deltas
1280 1302 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1281 1303 (tohash, tnhash, len(tn))]
1282 1304 for l in chunk(zlib.compress(tn)):
1283 1305 ret.append(fmtline(l))
1284 1306 ret.append('\n')
1285 1307 return ''.join(ret)
1286 1308
1287 1309 class GitDiffRequired(Exception):
1288 1310 pass
1289 1311
1290 1312 def diffopts(ui, opts=None, untrusted=False):
1291 1313 def get(key, name=None, getter=ui.configbool):
1292 1314 return ((opts and opts.get(key)) or
1293 1315 getter('diff', name or key, None, untrusted=untrusted))
1294 1316 return mdiff.diffopts(
1295 1317 text=opts and opts.get('text'),
1296 1318 git=get('git'),
1297 1319 nodates=get('nodates'),
1298 1320 showfunc=get('show_function', 'showfunc'),
1299 1321 ignorews=get('ignore_all_space', 'ignorews'),
1300 1322 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1301 1323 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1302 1324 context=get('unified', getter=ui.config))
1303 1325
1304 1326 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1305 1327 losedatafn=None, prefix=''):
1306 1328 '''yields diff of changes to files between two nodes, or node and
1307 1329 working directory.
1308 1330
1309 1331 if node1 is None, use first dirstate parent instead.
1310 1332 if node2 is None, compare node1 with working directory.
1311 1333
1312 1334 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1313 1335 every time some change cannot be represented with the current
1314 1336 patch format. Return False to upgrade to git patch format, True to
1315 1337 accept the loss or raise an exception to abort the diff. It is
1316 1338 called with the name of current file being diffed as 'fn'. If set
1317 1339 to None, patches will always be upgraded to git format when
1318 1340 necessary.
1319 1341
1320 1342 prefix is a filename prefix that is prepended to all filenames on
1321 1343 display (used for subrepos).
1322 1344 '''
1323 1345
1324 1346 if opts is None:
1325 1347 opts = mdiff.defaultopts
1326 1348
1327 1349 if not node1 and not node2:
1328 1350 node1 = repo.dirstate.p1()
1329 1351
1330 1352 def lrugetfilectx():
1331 1353 cache = {}
1332 1354 order = []
1333 1355 def getfilectx(f, ctx):
1334 1356 fctx = ctx.filectx(f, filelog=cache.get(f))
1335 1357 if f not in cache:
1336 1358 if len(cache) > 20:
1337 1359 del cache[order.pop(0)]
1338 1360 cache[f] = fctx.filelog()
1339 1361 else:
1340 1362 order.remove(f)
1341 1363 order.append(f)
1342 1364 return fctx
1343 1365 return getfilectx
1344 1366 getfilectx = lrugetfilectx()
1345 1367
1346 1368 ctx1 = repo[node1]
1347 1369 ctx2 = repo[node2]
1348 1370
1349 1371 if not changes:
1350 1372 changes = repo.status(ctx1, ctx2, match=match)
1351 1373 modified, added, removed = changes[:3]
1352 1374
1353 1375 if not modified and not added and not removed:
1354 1376 return []
1355 1377
1356 1378 revs = None
1357 1379 if not repo.ui.quiet:
1358 1380 hexfunc = repo.ui.debugflag and hex or short
1359 1381 revs = [hexfunc(node) for node in [node1, node2] if node]
1360 1382
1361 1383 copy = {}
1362 1384 if opts.git or opts.upgrade:
1363 1385 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1364 1386
1365 1387 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1366 1388 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1367 1389 if opts.upgrade and not opts.git:
1368 1390 try:
1369 1391 def losedata(fn):
1370 1392 if not losedatafn or not losedatafn(fn=fn):
1371 1393 raise GitDiffRequired()
1372 1394 # Buffer the whole output until we are sure it can be generated
1373 1395 return list(difffn(opts.copy(git=False), losedata))
1374 1396 except GitDiffRequired:
1375 1397 return difffn(opts.copy(git=True), None)
1376 1398 else:
1377 1399 return difffn(opts, None)
1378 1400
1379 1401 def difflabel(func, *args, **kw):
1380 1402 '''yields 2-tuples of (output, label) based on the output of func()'''
1381 1403 prefixes = [('diff', 'diff.diffline'),
1382 1404 ('copy', 'diff.extended'),
1383 1405 ('rename', 'diff.extended'),
1384 1406 ('old', 'diff.extended'),
1385 1407 ('new', 'diff.extended'),
1386 1408 ('deleted', 'diff.extended'),
1387 1409 ('---', 'diff.file_a'),
1388 1410 ('+++', 'diff.file_b'),
1389 1411 ('@@', 'diff.hunk'),
1390 1412 ('-', 'diff.deleted'),
1391 1413 ('+', 'diff.inserted')]
1392 1414
1393 1415 for chunk in func(*args, **kw):
1394 1416 lines = chunk.split('\n')
1395 1417 for i, line in enumerate(lines):
1396 1418 if i != 0:
1397 1419 yield ('\n', '')
1398 1420 stripline = line
1399 1421 if line and line[0] in '+-':
1400 1422 # highlight trailing whitespace, but only in changed lines
1401 1423 stripline = line.rstrip()
1402 1424 for prefix, label in prefixes:
1403 1425 if stripline.startswith(prefix):
1404 1426 yield (stripline, label)
1405 1427 break
1406 1428 else:
1407 1429 yield (line, '')
1408 1430 if line != stripline:
1409 1431 yield (line[len(stripline):], 'diff.trailingwhitespace')
1410 1432
1411 1433 def diffui(*args, **kw):
1412 1434 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1413 1435 return difflabel(diff, *args, **kw)
1414 1436
1415 1437
1416 1438 def _addmodehdr(header, omode, nmode):
1417 1439 if omode != nmode:
1418 1440 header.append('old mode %s\n' % omode)
1419 1441 header.append('new mode %s\n' % nmode)
1420 1442
1421 1443 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1422 1444 copy, getfilectx, opts, losedatafn, prefix):
1423 1445
1424 1446 def join(f):
1425 1447 return os.path.join(prefix, f)
1426 1448
1427 1449 date1 = util.datestr(ctx1.date())
1428 1450 man1 = ctx1.manifest()
1429 1451
1430 1452 gone = set()
1431 1453 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1432 1454
1433 1455 copyto = dict([(v, k) for k, v in copy.items()])
1434 1456
1435 1457 if opts.git:
1436 1458 revs = None
1437 1459
1438 1460 for f in sorted(modified + added + removed):
1439 1461 to = None
1440 1462 tn = None
1441 1463 dodiff = True
1442 1464 header = []
1443 1465 if f in man1:
1444 1466 to = getfilectx(f, ctx1).data()
1445 1467 if f not in removed:
1446 1468 tn = getfilectx(f, ctx2).data()
1447 1469 a, b = f, f
1448 1470 if opts.git or losedatafn:
1449 1471 if f in added:
1450 1472 mode = gitmode[ctx2.flags(f)]
1451 1473 if f in copy or f in copyto:
1452 1474 if opts.git:
1453 1475 if f in copy:
1454 1476 a = copy[f]
1455 1477 else:
1456 1478 a = copyto[f]
1457 1479 omode = gitmode[man1.flags(a)]
1458 1480 _addmodehdr(header, omode, mode)
1459 1481 if a in removed and a not in gone:
1460 1482 op = 'rename'
1461 1483 gone.add(a)
1462 1484 else:
1463 1485 op = 'copy'
1464 1486 header.append('%s from %s\n' % (op, join(a)))
1465 1487 header.append('%s to %s\n' % (op, join(f)))
1466 1488 to = getfilectx(a, ctx1).data()
1467 1489 else:
1468 1490 losedatafn(f)
1469 1491 else:
1470 1492 if opts.git:
1471 1493 header.append('new file mode %s\n' % mode)
1472 1494 elif ctx2.flags(f):
1473 1495 losedatafn(f)
1474 1496 # In theory, if tn was copied or renamed we should check
1475 1497 # if the source is binary too but the copy record already
1476 1498 # forces git mode.
1477 1499 if util.binary(tn):
1478 1500 if opts.git:
1479 1501 dodiff = 'binary'
1480 1502 else:
1481 1503 losedatafn(f)
1482 1504 if not opts.git and not tn:
1483 1505 # regular diffs cannot represent new empty file
1484 1506 losedatafn(f)
1485 1507 elif f in removed:
1486 1508 if opts.git:
1487 1509 # have we already reported a copy above?
1488 1510 if ((f in copy and copy[f] in added
1489 1511 and copyto[copy[f]] == f) or
1490 1512 (f in copyto and copyto[f] in added
1491 1513 and copy[copyto[f]] == f)):
1492 1514 dodiff = False
1493 1515 else:
1494 1516 header.append('deleted file mode %s\n' %
1495 1517 gitmode[man1.flags(f)])
1496 1518 elif not to or util.binary(to):
1497 1519 # regular diffs cannot represent empty file deletion
1498 1520 losedatafn(f)
1499 1521 else:
1500 1522 oflag = man1.flags(f)
1501 1523 nflag = ctx2.flags(f)
1502 1524 binary = util.binary(to) or util.binary(tn)
1503 1525 if opts.git:
1504 1526 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1505 1527 if binary:
1506 1528 dodiff = 'binary'
1507 1529 elif binary or nflag != oflag:
1508 1530 losedatafn(f)
1509 1531 if opts.git:
1510 1532 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1511 1533
1512 1534 if dodiff:
1513 1535 if dodiff == 'binary':
1514 1536 text = b85diff(to, tn)
1515 1537 else:
1516 1538 text = mdiff.unidiff(to, date1,
1517 1539 # ctx2 date may be dynamic
1518 1540 tn, util.datestr(ctx2.date()),
1519 1541 join(a), join(b), revs, opts=opts)
1520 1542 if header and (text or len(header) > 1):
1521 1543 yield ''.join(header)
1522 1544 if text:
1523 1545 yield text
1524 1546
1525 1547 def diffstatdata(lines):
1526 1548 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1527 1549
1528 1550 filename, adds, removes = None, 0, 0
1529 1551 for line in lines:
1530 1552 if line.startswith('diff'):
1531 1553 if filename:
1532 1554 isbinary = adds == 0 and removes == 0
1533 1555 yield (filename, adds, removes, isbinary)
1534 1556 # set numbers to 0 anyway when starting new file
1535 1557 adds, removes = 0, 0
1536 1558 if line.startswith('diff --git'):
1537 1559 filename = gitre.search(line).group(1)
1538 1560 elif line.startswith('diff -r'):
1539 1561 # format: "diff -r ... -r ... filename"
1540 1562 filename = diffre.search(line).group(1)
1541 1563 elif line.startswith('+') and not line.startswith('+++'):
1542 1564 adds += 1
1543 1565 elif line.startswith('-') and not line.startswith('---'):
1544 1566 removes += 1
1545 1567 if filename:
1546 1568 isbinary = adds == 0 and removes == 0
1547 1569 yield (filename, adds, removes, isbinary)
1548 1570
1549 1571 def diffstat(lines, width=80, git=False):
1550 1572 output = []
1551 1573 stats = list(diffstatdata(lines))
1552 1574
1553 1575 maxtotal, maxname = 0, 0
1554 1576 totaladds, totalremoves = 0, 0
1555 1577 hasbinary = False
1556 1578
1557 1579 sized = [(filename, adds, removes, isbinary, encoding.colwidth(filename))
1558 1580 for filename, adds, removes, isbinary in stats]
1559 1581
1560 1582 for filename, adds, removes, isbinary, namewidth in sized:
1561 1583 totaladds += adds
1562 1584 totalremoves += removes
1563 1585 maxname = max(maxname, namewidth)
1564 1586 maxtotal = max(maxtotal, adds + removes)
1565 1587 if isbinary:
1566 1588 hasbinary = True
1567 1589
1568 1590 countwidth = len(str(maxtotal))
1569 1591 if hasbinary and countwidth < 3:
1570 1592 countwidth = 3
1571 1593 graphwidth = width - countwidth - maxname - 6
1572 1594 if graphwidth < 10:
1573 1595 graphwidth = 10
1574 1596
1575 1597 def scale(i):
1576 1598 if maxtotal <= graphwidth:
1577 1599 return i
1578 1600 # If diffstat runs out of room it doesn't print anything,
1579 1601 # which isn't very useful, so always print at least one + or -
1580 1602 # if there were at least some changes.
1581 1603 return max(i * graphwidth // maxtotal, int(bool(i)))
1582 1604
1583 1605 for filename, adds, removes, isbinary, namewidth in sized:
1584 1606 if git and isbinary:
1585 1607 count = 'Bin'
1586 1608 else:
1587 1609 count = adds + removes
1588 1610 pluses = '+' * scale(adds)
1589 1611 minuses = '-' * scale(removes)
1590 1612 output.append(' %s%s | %*s %s%s\n' %
1591 1613 (filename, ' ' * (maxname - namewidth),
1592 1614 countwidth, count,
1593 1615 pluses, minuses))
1594 1616
1595 1617 if stats:
1596 1618 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1597 1619 % (len(stats), totaladds, totalremoves))
1598 1620
1599 1621 return ''.join(output)
1600 1622
1601 1623 def diffstatui(*args, **kw):
1602 1624 '''like diffstat(), but yields 2-tuples of (output, label) for
1603 1625 ui.write()
1604 1626 '''
1605 1627
1606 1628 for line in diffstat(*args, **kw).splitlines():
1607 1629 if line and line[-1] in '+-':
1608 1630 name, graph = line.rsplit(' ', 1)
1609 1631 yield (name + ' ', '')
1610 1632 m = re.search(r'\++', graph)
1611 1633 if m:
1612 1634 yield (m.group(0), 'diffstat.inserted')
1613 1635 m = re.search(r'-+', graph)
1614 1636 if m:
1615 1637 yield (m.group(0), 'diffstat.deleted')
1616 1638 else:
1617 1639 yield (line, '')
1618 1640 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now