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