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