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