##// END OF EJS Templates
patch: strip paths in leaked git patchmeta objects
Mads Kiilerich -
r11022:0429d0d4 default
parent child Browse files
Show More
@@ -1,1706 +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
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, 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 write(self, dest=None):
531 531 if not self.dirty:
532 532 return
533 533 if not dest:
534 534 dest = self.fname
535 535 self.writelines(dest, self.lines)
536 536
537 537 def close(self):
538 538 self.write()
539 539 self.write_rej()
540 540
541 541 def apply(self, h):
542 542 if not h.complete():
543 543 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
544 544 (h.number, h.desc, len(h.a), h.lena, len(h.b),
545 545 h.lenb))
546 546
547 547 self.hunks += 1
548 548
549 549 if self.missing:
550 550 self.rej.append(h)
551 551 return -1
552 552
553 553 if self.exists and h.createfile():
554 554 self.ui.warn(_("file %s already exists\n") % self.fname)
555 555 self.rej.append(h)
556 556 return -1
557 557
558 558 if isinstance(h, binhunk):
559 559 if h.rmfile():
560 560 self.unlink(self.fname)
561 561 else:
562 562 self.lines[:] = h.new()
563 563 self.offset += len(h.new())
564 564 self.dirty = 1
565 565 return 0
566 566
567 567 horig = h
568 568 if (self.eolmode in ('crlf', 'lf')
569 569 or self.eolmode == 'auto' and self.eol):
570 570 # If new eols are going to be normalized, then normalize
571 571 # hunk data before patching. Otherwise, preserve input
572 572 # line-endings.
573 573 h = h.getnormalized()
574 574
575 575 # fast case first, no offsets, no fuzz
576 576 old = h.old()
577 577 # patch starts counting at 1 unless we are adding the file
578 578 if h.starta == 0:
579 579 start = 0
580 580 else:
581 581 start = h.starta + self.offset - 1
582 582 orig_start = start
583 583 # if there's skew we want to emit the "(offset %d lines)" even
584 584 # when the hunk cleanly applies at start + skew, so skip the
585 585 # fast case code
586 586 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
587 587 if h.rmfile():
588 588 self.unlink(self.fname)
589 589 else:
590 590 self.lines[start : start + h.lena] = h.new()
591 591 self.offset += h.lenb - h.lena
592 592 self.dirty = 1
593 593 return 0
594 594
595 595 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
596 596 self.hashlines()
597 597 if h.hunk[-1][0] != ' ':
598 598 # if the hunk tried to put something at the bottom of the file
599 599 # override the start line and use eof here
600 600 search_start = len(self.lines)
601 601 else:
602 602 search_start = orig_start + self.skew
603 603
604 604 for fuzzlen in xrange(3):
605 605 for toponly in [True, False]:
606 606 old = h.old(fuzzlen, toponly)
607 607
608 608 cand = self.findlines(old[0][1:], search_start)
609 609 for l in cand:
610 610 if diffhelpers.testhunk(old, self.lines, l) == 0:
611 611 newlines = h.new(fuzzlen, toponly)
612 612 self.lines[l : l + len(old)] = newlines
613 613 self.offset += len(newlines) - len(old)
614 614 self.skew = l - orig_start
615 615 self.dirty = 1
616 616 offset = l - orig_start - fuzzlen
617 617 if fuzzlen:
618 618 msg = _("Hunk #%d succeeded at %d "
619 619 "with fuzz %d "
620 620 "(offset %d lines).\n")
621 621 self.printfile(True)
622 622 self.ui.warn(msg %
623 623 (h.number, l + 1, fuzzlen, offset))
624 624 else:
625 625 msg = _("Hunk #%d succeeded at %d "
626 626 "(offset %d lines).\n")
627 627 self.ui.note(msg % (h.number, l + 1, offset))
628 628 return fuzzlen
629 629 self.printfile(True)
630 630 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
631 631 self.rej.append(horig)
632 632 return -1
633 633
634 634 class hunk(object):
635 635 def __init__(self, desc, num, lr, context, create=False, remove=False):
636 636 self.number = num
637 637 self.desc = desc
638 638 self.hunk = [desc]
639 639 self.a = []
640 640 self.b = []
641 641 self.starta = self.lena = None
642 642 self.startb = self.lenb = None
643 643 if lr is not None:
644 644 if context:
645 645 self.read_context_hunk(lr)
646 646 else:
647 647 self.read_unified_hunk(lr)
648 648 self.create = create
649 649 self.remove = remove and not create
650 650
651 651 def getnormalized(self):
652 652 """Return a copy with line endings normalized to LF."""
653 653
654 654 def normalize(lines):
655 655 nlines = []
656 656 for line in lines:
657 657 if line.endswith('\r\n'):
658 658 line = line[:-2] + '\n'
659 659 nlines.append(line)
660 660 return nlines
661 661
662 662 # Dummy object, it is rebuilt manually
663 663 nh = hunk(self.desc, self.number, None, None, False, False)
664 664 nh.number = self.number
665 665 nh.desc = self.desc
666 666 nh.hunk = self.hunk
667 667 nh.a = normalize(self.a)
668 668 nh.b = normalize(self.b)
669 669 nh.starta = self.starta
670 670 nh.startb = self.startb
671 671 nh.lena = self.lena
672 672 nh.lenb = self.lenb
673 673 nh.create = self.create
674 674 nh.remove = self.remove
675 675 return nh
676 676
677 677 def read_unified_hunk(self, lr):
678 678 m = unidesc.match(self.desc)
679 679 if not m:
680 680 raise PatchError(_("bad hunk #%d") % self.number)
681 681 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
682 682 if self.lena is None:
683 683 self.lena = 1
684 684 else:
685 685 self.lena = int(self.lena)
686 686 if self.lenb is None:
687 687 self.lenb = 1
688 688 else:
689 689 self.lenb = int(self.lenb)
690 690 self.starta = int(self.starta)
691 691 self.startb = int(self.startb)
692 692 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
693 693 # if we hit eof before finishing out the hunk, the last line will
694 694 # be zero length. Lets try to fix it up.
695 695 while len(self.hunk[-1]) == 0:
696 696 del self.hunk[-1]
697 697 del self.a[-1]
698 698 del self.b[-1]
699 699 self.lena -= 1
700 700 self.lenb -= 1
701 701
702 702 def read_context_hunk(self, lr):
703 703 self.desc = lr.readline()
704 704 m = contextdesc.match(self.desc)
705 705 if not m:
706 706 raise PatchError(_("bad hunk #%d") % self.number)
707 707 foo, self.starta, foo2, aend, foo3 = m.groups()
708 708 self.starta = int(self.starta)
709 709 if aend is None:
710 710 aend = self.starta
711 711 self.lena = int(aend) - self.starta
712 712 if self.starta:
713 713 self.lena += 1
714 714 for x in xrange(self.lena):
715 715 l = lr.readline()
716 716 if l.startswith('---'):
717 717 lr.push(l)
718 718 break
719 719 s = l[2:]
720 720 if l.startswith('- ') or l.startswith('! '):
721 721 u = '-' + s
722 722 elif l.startswith(' '):
723 723 u = ' ' + s
724 724 else:
725 725 raise PatchError(_("bad hunk #%d old text line %d") %
726 726 (self.number, x))
727 727 self.a.append(u)
728 728 self.hunk.append(u)
729 729
730 730 l = lr.readline()
731 731 if l.startswith('\ '):
732 732 s = self.a[-1][:-1]
733 733 self.a[-1] = s
734 734 self.hunk[-1] = s
735 735 l = lr.readline()
736 736 m = contextdesc.match(l)
737 737 if not m:
738 738 raise PatchError(_("bad hunk #%d") % self.number)
739 739 foo, self.startb, foo2, bend, foo3 = m.groups()
740 740 self.startb = int(self.startb)
741 741 if bend is None:
742 742 bend = self.startb
743 743 self.lenb = int(bend) - self.startb
744 744 if self.startb:
745 745 self.lenb += 1
746 746 hunki = 1
747 747 for x in xrange(self.lenb):
748 748 l = lr.readline()
749 749 if l.startswith('\ '):
750 750 s = self.b[-1][:-1]
751 751 self.b[-1] = s
752 752 self.hunk[hunki - 1] = s
753 753 continue
754 754 if not l:
755 755 lr.push(l)
756 756 break
757 757 s = l[2:]
758 758 if l.startswith('+ ') or l.startswith('! '):
759 759 u = '+' + s
760 760 elif l.startswith(' '):
761 761 u = ' ' + s
762 762 elif len(self.b) == 0:
763 763 # this can happen when the hunk does not add any lines
764 764 lr.push(l)
765 765 break
766 766 else:
767 767 raise PatchError(_("bad hunk #%d old text line %d") %
768 768 (self.number, x))
769 769 self.b.append(s)
770 770 while True:
771 771 if hunki >= len(self.hunk):
772 772 h = ""
773 773 else:
774 774 h = self.hunk[hunki]
775 775 hunki += 1
776 776 if h == u:
777 777 break
778 778 elif h.startswith('-'):
779 779 continue
780 780 else:
781 781 self.hunk.insert(hunki - 1, u)
782 782 break
783 783
784 784 if not self.a:
785 785 # this happens when lines were only added to the hunk
786 786 for x in self.hunk:
787 787 if x.startswith('-') or x.startswith(' '):
788 788 self.a.append(x)
789 789 if not self.b:
790 790 # this happens when lines were only deleted from the hunk
791 791 for x in self.hunk:
792 792 if x.startswith('+') or x.startswith(' '):
793 793 self.b.append(x[1:])
794 794 # @@ -start,len +start,len @@
795 795 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
796 796 self.startb, self.lenb)
797 797 self.hunk[0] = self.desc
798 798
799 799 def fix_newline(self):
800 800 diffhelpers.fix_newline(self.hunk, self.a, self.b)
801 801
802 802 def complete(self):
803 803 return len(self.a) == self.lena and len(self.b) == self.lenb
804 804
805 805 def createfile(self):
806 806 return self.starta == 0 and self.lena == 0 and self.create
807 807
808 808 def rmfile(self):
809 809 return self.startb == 0 and self.lenb == 0 and self.remove
810 810
811 811 def fuzzit(self, l, fuzz, toponly):
812 812 # this removes context lines from the top and bottom of list 'l'. It
813 813 # checks the hunk to make sure only context lines are removed, and then
814 814 # returns a new shortened list of lines.
815 815 fuzz = min(fuzz, len(l)-1)
816 816 if fuzz:
817 817 top = 0
818 818 bot = 0
819 819 hlen = len(self.hunk)
820 820 for x in xrange(hlen - 1):
821 821 # the hunk starts with the @@ line, so use x+1
822 822 if self.hunk[x + 1][0] == ' ':
823 823 top += 1
824 824 else:
825 825 break
826 826 if not toponly:
827 827 for x in xrange(hlen - 1):
828 828 if self.hunk[hlen - bot - 1][0] == ' ':
829 829 bot += 1
830 830 else:
831 831 break
832 832
833 833 # top and bot now count context in the hunk
834 834 # adjust them if either one is short
835 835 context = max(top, bot, 3)
836 836 if bot < context:
837 837 bot = max(0, fuzz - (context - bot))
838 838 else:
839 839 bot = min(fuzz, bot)
840 840 if top < context:
841 841 top = max(0, fuzz - (context - top))
842 842 else:
843 843 top = min(fuzz, top)
844 844
845 845 return l[top:len(l)-bot]
846 846 return l
847 847
848 848 def old(self, fuzz=0, toponly=False):
849 849 return self.fuzzit(self.a, fuzz, toponly)
850 850
851 851 def new(self, fuzz=0, toponly=False):
852 852 return self.fuzzit(self.b, fuzz, toponly)
853 853
854 854 class binhunk:
855 855 'A binary patch file. Only understands literals so far.'
856 856 def __init__(self, gitpatch):
857 857 self.gitpatch = gitpatch
858 858 self.text = None
859 859 self.hunk = ['GIT binary patch\n']
860 860
861 861 def createfile(self):
862 862 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
863 863
864 864 def rmfile(self):
865 865 return self.gitpatch.op == 'DELETE'
866 866
867 867 def complete(self):
868 868 return self.text is not None
869 869
870 870 def new(self):
871 871 return [self.text]
872 872
873 873 def extract(self, lr):
874 874 line = lr.readline()
875 875 self.hunk.append(line)
876 876 while line and not line.startswith('literal '):
877 877 line = lr.readline()
878 878 self.hunk.append(line)
879 879 if not line:
880 880 raise PatchError(_('could not extract binary patch'))
881 881 size = int(line[8:].rstrip())
882 882 dec = []
883 883 line = lr.readline()
884 884 self.hunk.append(line)
885 885 while len(line) > 1:
886 886 l = line[0]
887 887 if l <= 'Z' and l >= 'A':
888 888 l = ord(l) - ord('A') + 1
889 889 else:
890 890 l = ord(l) - ord('a') + 27
891 891 dec.append(base85.b85decode(line[1:-1])[:l])
892 892 line = lr.readline()
893 893 self.hunk.append(line)
894 894 text = zlib.decompress(''.join(dec))
895 895 if len(text) != size:
896 896 raise PatchError(_('binary patch is %d bytes, not %d') %
897 897 len(text), size)
898 898 self.text = text
899 899
900 900 def parsefilename(str):
901 901 # --- filename \t|space stuff
902 902 s = str[4:].rstrip('\r\n')
903 903 i = s.find('\t')
904 904 if i < 0:
905 905 i = s.find(' ')
906 906 if i < 0:
907 907 return s
908 908 return s[:i]
909 909
910 def pathstrip(path, strip):
911 pathlen = len(path)
912 i = 0
913 if strip == 0:
914 return '', path.rstrip()
915 count = strip
916 while count > 0:
917 i = path.find('/', i)
918 if i == -1:
919 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
920 (count, strip, path))
921 i += 1
922 # consume '//' in the path
923 while i < pathlen - 1 and path[i] == '/':
924 i += 1
925 count -= 1
926 return path[:i].lstrip(), path[i:].rstrip()
927
910 928 def selectfile(afile_orig, bfile_orig, hunk, strip):
911 def pathstrip(path, strip):
912 pathlen = len(path)
913 i = 0
914 if strip == 0:
915 return '', path.rstrip()
916 count = strip
917 while count > 0:
918 i = path.find('/', i)
919 if i == -1:
920 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
921 (count, strip, path))
922 i += 1
923 # consume '//' in the path
924 while i < pathlen - 1 and path[i] == '/':
925 i += 1
926 count -= 1
927 return path[:i].lstrip(), path[i:].rstrip()
928
929 929 nulla = afile_orig == "/dev/null"
930 930 nullb = bfile_orig == "/dev/null"
931 931 abase, afile = pathstrip(afile_orig, strip)
932 932 gooda = not nulla and util.lexists(afile)
933 933 bbase, bfile = pathstrip(bfile_orig, strip)
934 934 if afile == bfile:
935 935 goodb = gooda
936 936 else:
937 937 goodb = not nullb and os.path.exists(bfile)
938 938 createfunc = hunk.createfile
939 939 missing = not goodb and not gooda and not createfunc()
940 940
941 941 # some diff programs apparently produce create patches where the
942 942 # afile is not /dev/null, but afile starts with bfile
943 943 abasedir = afile[:afile.rfind('/') + 1]
944 944 bbasedir = bfile[:bfile.rfind('/') + 1]
945 945 if missing and abasedir == bbasedir and afile.startswith(bfile):
946 946 # this isn't very pretty
947 947 hunk.create = True
948 948 if createfunc():
949 949 missing = False
950 950 else:
951 951 hunk.create = False
952 952
953 953 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
954 954 # diff is between a file and its backup. In this case, the original
955 955 # file should be patched (see original mpatch code).
956 956 isbackup = (abase == bbase and bfile.startswith(afile))
957 957 fname = None
958 958 if not missing:
959 959 if gooda and goodb:
960 960 fname = isbackup and afile or bfile
961 961 elif gooda:
962 962 fname = afile
963 963
964 964 if not fname:
965 965 if not nullb:
966 966 fname = isbackup and afile or bfile
967 967 elif not nulla:
968 968 fname = afile
969 969 else:
970 970 raise PatchError(_("undefined source and destination files"))
971 971
972 972 return fname, missing
973 973
974 974 def scangitpatch(lr, firstline):
975 975 """
976 976 Git patches can emit:
977 977 - rename a to b
978 978 - change b
979 979 - copy a to c
980 980 - change c
981 981
982 982 We cannot apply this sequence as-is, the renamed 'a' could not be
983 983 found for it would have been renamed already. And we cannot copy
984 984 from 'b' instead because 'b' would have been changed already. So
985 985 we scan the git patch for copy and rename commands so we can
986 986 perform the copies ahead of time.
987 987 """
988 988 pos = 0
989 989 try:
990 990 pos = lr.fp.tell()
991 991 fp = lr.fp
992 992 except IOError:
993 993 fp = cStringIO.StringIO(lr.fp.read())
994 994 gitlr = linereader(fp, lr.textmode)
995 995 gitlr.push(firstline)
996 996 (dopatch, gitpatches) = readgitpatch(gitlr)
997 997 fp.seek(pos)
998 998 return dopatch, gitpatches
999 999
1000 1000 def iterhunks(ui, fp, sourcefile=None):
1001 1001 """Read a patch and yield the following events:
1002 1002 - ("file", afile, bfile, firsthunk): select a new target file.
1003 1003 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1004 1004 "file" event.
1005 1005 - ("git", gitchanges): current diff is in git format, gitchanges
1006 1006 maps filenames to gitpatch records. Unique event.
1007 1007 """
1008 1008 changed = {}
1009 1009 current_hunk = None
1010 1010 afile = ""
1011 1011 bfile = ""
1012 1012 state = None
1013 1013 hunknum = 0
1014 1014 emitfile = False
1015 1015 git = False
1016 1016
1017 1017 # our states
1018 1018 BFILE = 1
1019 1019 context = None
1020 1020 lr = linereader(fp)
1021 1021 # gitworkdone is True if a git operation (copy, rename, ...) was
1022 1022 # performed already for the current file. Useful when the file
1023 1023 # section may have no hunk.
1024 1024 gitworkdone = False
1025 1025 empty = None
1026 1026
1027 1027 while True:
1028 1028 newfile = newgitfile = False
1029 1029 x = lr.readline()
1030 1030 if not x:
1031 1031 break
1032 1032 if current_hunk:
1033 1033 if x.startswith('\ '):
1034 1034 current_hunk.fix_newline()
1035 1035 yield 'hunk', current_hunk
1036 1036 current_hunk = None
1037 1037 empty = False
1038 1038 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
1039 1039 ((context is not False) and x.startswith('***************')))):
1040 1040 try:
1041 1041 if context is None and x.startswith('***************'):
1042 1042 context = True
1043 1043 gpatch = changed.get(bfile)
1044 1044 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
1045 1045 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
1046 1046 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
1047 1047 except PatchError, err:
1048 1048 ui.debug(err)
1049 1049 current_hunk = None
1050 1050 continue
1051 1051 hunknum += 1
1052 1052 if emitfile:
1053 1053 emitfile = False
1054 1054 yield 'file', (afile, bfile, current_hunk)
1055 1055 empty = False
1056 1056 elif state == BFILE and x.startswith('GIT binary patch'):
1057 1057 current_hunk = binhunk(changed[bfile])
1058 1058 hunknum += 1
1059 1059 if emitfile:
1060 1060 emitfile = False
1061 1061 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
1062 1062 empty = False
1063 1063 current_hunk.extract(lr)
1064 1064 elif x.startswith('diff --git'):
1065 1065 # check for git diff, scanning the whole patch file if needed
1066 1066 m = gitre.match(x)
1067 1067 gitworkdone = False
1068 1068 if m:
1069 1069 afile, bfile = m.group(1, 2)
1070 1070 if not git:
1071 1071 git = True
1072 1072 gitpatches = scangitpatch(lr, x)[1]
1073 1073 yield 'git', gitpatches
1074 1074 for gp in gitpatches:
1075 1075 changed[gp.path] = gp
1076 1076 # else error?
1077 1077 # copy/rename + modify should modify target, not source
1078 1078 gp = changed.get(bfile)
1079 1079 if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD')
1080 1080 or gp.mode):
1081 1081 afile = bfile
1082 1082 gitworkdone = True
1083 1083 newgitfile = True
1084 1084 elif x.startswith('---'):
1085 1085 # check for a unified diff
1086 1086 l2 = lr.readline()
1087 1087 if not l2.startswith('+++'):
1088 1088 lr.push(l2)
1089 1089 continue
1090 1090 newfile = True
1091 1091 context = False
1092 1092 afile = parsefilename(x)
1093 1093 bfile = parsefilename(l2)
1094 1094 elif x.startswith('***'):
1095 1095 # check for a context diff
1096 1096 l2 = lr.readline()
1097 1097 if not l2.startswith('---'):
1098 1098 lr.push(l2)
1099 1099 continue
1100 1100 l3 = lr.readline()
1101 1101 lr.push(l3)
1102 1102 if not l3.startswith("***************"):
1103 1103 lr.push(l2)
1104 1104 continue
1105 1105 newfile = True
1106 1106 context = True
1107 1107 afile = parsefilename(x)
1108 1108 bfile = parsefilename(l2)
1109 1109
1110 1110 if newfile:
1111 1111 if empty:
1112 1112 raise NoHunks
1113 1113 empty = not gitworkdone
1114 1114 gitworkdone = False
1115 1115
1116 1116 if newgitfile or newfile:
1117 1117 emitfile = True
1118 1118 state = BFILE
1119 1119 hunknum = 0
1120 1120 if current_hunk:
1121 1121 if current_hunk.complete():
1122 1122 yield 'hunk', current_hunk
1123 1123 empty = False
1124 1124 else:
1125 1125 raise PatchError(_("malformed patch %s %s") % (afile,
1126 1126 current_hunk.desc))
1127 1127
1128 1128 if (empty is None and not gitworkdone) or empty:
1129 1129 raise NoHunks
1130 1130
1131 1131
1132 1132 def applydiff(ui, fp, changed, strip=1, sourcefile=None, eolmode='strict'):
1133 1133 """Reads a patch from fp and tries to apply it.
1134 1134
1135 1135 The dict 'changed' is filled in with all of the filenames changed
1136 1136 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1137 1137 found and 1 if there was any fuzz.
1138 1138
1139 1139 If 'eolmode' is 'strict', the patch content and patched file are
1140 1140 read in binary mode. Otherwise, line endings are ignored when
1141 1141 patching then normalized according to 'eolmode'.
1142 1142
1143 1143 Callers probably want to call 'updatedir' after this to apply
1144 1144 certain categories of changes not done by this function.
1145 1145 """
1146 1146 return _applydiff(
1147 1147 ui, fp, patchfile, copyfile,
1148 1148 changed, strip=strip, sourcefile=sourcefile, eolmode=eolmode)
1149 1149
1150 1150
1151 1151 def _applydiff(ui, fp, patcher, copyfn, changed, strip=1,
1152 1152 sourcefile=None, eolmode='strict'):
1153 1153 rejects = 0
1154 1154 err = 0
1155 1155 current_file = None
1156 1156 cwd = os.getcwd()
1157 1157 opener = util.opener(cwd)
1158 1158
1159 1159 def closefile():
1160 1160 if not current_file:
1161 1161 return 0
1162 1162 current_file.close()
1163 1163 return len(current_file.rej)
1164 1164
1165 1165 for state, values in iterhunks(ui, fp, sourcefile):
1166 1166 if state == 'hunk':
1167 1167 if not current_file:
1168 1168 continue
1169 1169 ret = current_file.apply(values)
1170 1170 if ret >= 0:
1171 1171 changed.setdefault(current_file.fname, None)
1172 1172 if ret > 0:
1173 1173 err = 1
1174 1174 elif state == 'file':
1175 1175 rejects += closefile()
1176 1176 afile, bfile, first_hunk = values
1177 1177 try:
1178 1178 if sourcefile:
1179 1179 current_file = patcher(ui, sourcefile, opener,
1180 1180 eolmode=eolmode)
1181 1181 else:
1182 1182 current_file, missing = selectfile(afile, bfile,
1183 1183 first_hunk, strip)
1184 1184 current_file = patcher(ui, current_file, opener,
1185 1185 missing=missing, eolmode=eolmode)
1186 1186 except PatchError, err:
1187 1187 ui.warn(str(err) + '\n')
1188 1188 current_file = None
1189 1189 rejects += 1
1190 1190 continue
1191 1191 elif state == 'git':
1192 1192 for gp in values:
1193 gp.path = pathstrip(gp.path, strip - 1)[1]
1194 if gp.oldpath:
1195 gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1]
1193 1196 if gp.op in ('COPY', 'RENAME'):
1194 1197 copyfn(gp.oldpath, gp.path, cwd)
1195 1198 changed[gp.path] = gp
1196 1199 else:
1197 1200 raise util.Abort(_('unsupported parser state: %s') % state)
1198 1201
1199 1202 rejects += closefile()
1200 1203
1201 1204 if rejects:
1202 1205 return -1
1203 1206 return err
1204 1207
1205 1208 def updatedir(ui, repo, patches, similarity=0):
1206 1209 '''Update dirstate after patch application according to metadata'''
1207 1210 if not patches:
1208 1211 return
1209 1212 copies = []
1210 1213 removes = set()
1211 1214 cfiles = patches.keys()
1212 1215 cwd = repo.getcwd()
1213 1216 if cwd:
1214 1217 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1215 1218 for f in patches:
1216 1219 gp = patches[f]
1217 1220 if not gp:
1218 1221 continue
1219 1222 if gp.op == 'RENAME':
1220 1223 copies.append((gp.oldpath, gp.path))
1221 1224 removes.add(gp.oldpath)
1222 1225 elif gp.op == 'COPY':
1223 1226 copies.append((gp.oldpath, gp.path))
1224 1227 elif gp.op == 'DELETE':
1225 1228 removes.add(gp.path)
1226 1229 for src, dst in copies:
1227 1230 repo.copy(src, dst)
1228 1231 if (not similarity) and removes:
1229 1232 repo.remove(sorted(removes), True)
1230 1233 for f in patches:
1231 1234 gp = patches[f]
1232 1235 if gp and gp.mode:
1233 1236 islink, isexec = gp.mode
1234 1237 dst = repo.wjoin(gp.path)
1235 1238 # patch won't create empty files
1236 1239 if gp.op == 'ADD' and not os.path.exists(dst):
1237 1240 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1238 1241 repo.wwrite(gp.path, '', flags)
1239 1242 util.set_flags(dst, islink, isexec)
1240 1243 cmdutil.addremove(repo, cfiles, similarity=similarity)
1241 1244 files = patches.keys()
1242 1245 files.extend([r for r in removes if r not in files])
1243 1246 return sorted(files)
1244 1247
1245 1248 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1246 1249 """use <patcher> to apply <patchname> to the working directory.
1247 1250 returns whether patch was applied with fuzz factor."""
1248 1251
1249 1252 fuzz = False
1250 1253 if cwd:
1251 1254 args.append('-d %s' % util.shellquote(cwd))
1252 1255 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1253 1256 util.shellquote(patchname)))
1254 1257
1255 1258 for line in fp:
1256 1259 line = line.rstrip()
1257 1260 ui.note(line + '\n')
1258 1261 if line.startswith('patching file '):
1259 1262 pf = util.parse_patch_output(line)
1260 1263 printed_file = False
1261 1264 files.setdefault(pf, None)
1262 1265 elif line.find('with fuzz') >= 0:
1263 1266 fuzz = True
1264 1267 if not printed_file:
1265 1268 ui.warn(pf + '\n')
1266 1269 printed_file = True
1267 1270 ui.warn(line + '\n')
1268 1271 elif line.find('saving rejects to file') >= 0:
1269 1272 ui.warn(line + '\n')
1270 1273 elif line.find('FAILED') >= 0:
1271 1274 if not printed_file:
1272 1275 ui.warn(pf + '\n')
1273 1276 printed_file = True
1274 1277 ui.warn(line + '\n')
1275 1278 code = fp.close()
1276 1279 if code:
1277 1280 raise PatchError(_("patch command failed: %s") %
1278 1281 util.explain_exit(code)[0])
1279 1282 return fuzz
1280 1283
1281 1284 def internalpatch(patchobj, ui, strip, cwd, files=None, eolmode='strict'):
1282 1285 """use builtin patch to apply <patchobj> to the working directory.
1283 1286 returns whether patch was applied with fuzz factor."""
1284 1287
1285 1288 if files is None:
1286 1289 files = {}
1287 1290 if eolmode is None:
1288 1291 eolmode = ui.config('patch', 'eol', 'strict')
1289 1292 if eolmode.lower() not in eolmodes:
1290 1293 raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
1291 1294 eolmode = eolmode.lower()
1292 1295
1293 1296 try:
1294 1297 fp = open(patchobj, 'rb')
1295 1298 except TypeError:
1296 1299 fp = patchobj
1297 1300 if cwd:
1298 1301 curdir = os.getcwd()
1299 1302 os.chdir(cwd)
1300 1303 try:
1301 1304 ret = applydiff(ui, fp, files, strip=strip, eolmode=eolmode)
1302 1305 finally:
1303 1306 if cwd:
1304 1307 os.chdir(curdir)
1305 1308 if fp != patchobj:
1306 1309 fp.close()
1307 1310 if ret < 0:
1308 1311 raise PatchError
1309 1312 return ret > 0
1310 1313
1311 1314 def patch(patchname, ui, strip=1, cwd=None, files=None, eolmode='strict'):
1312 1315 """Apply <patchname> to the working directory.
1313 1316
1314 1317 'eolmode' specifies how end of lines should be handled. It can be:
1315 1318 - 'strict': inputs are read in binary mode, EOLs are preserved
1316 1319 - 'crlf': EOLs are ignored when patching and reset to CRLF
1317 1320 - 'lf': EOLs are ignored when patching and reset to LF
1318 1321 - None: get it from user settings, default to 'strict'
1319 1322 'eolmode' is ignored when using an external patcher program.
1320 1323
1321 1324 Returns whether patch was applied with fuzz factor.
1322 1325 """
1323 1326 patcher = ui.config('ui', 'patch')
1324 1327 args = []
1325 1328 if files is None:
1326 1329 files = {}
1327 1330 try:
1328 1331 if patcher:
1329 1332 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1330 1333 files)
1331 1334 else:
1332 1335 try:
1333 1336 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1334 1337 except NoHunks:
1335 1338 ui.warn(_('internal patcher failed\n'
1336 1339 'please report details to '
1337 1340 'http://mercurial.selenic.com/bts/\n'
1338 1341 'or mercurial@selenic.com\n'))
1339 1342 patcher = (util.find_exe('gpatch') or util.find_exe('patch')
1340 1343 or 'patch')
1341 1344 ui.debug('no valid hunks found; trying with %r instead\n' %
1342 1345 patcher)
1343 1346 if util.needbinarypatch():
1344 1347 args.append('--binary')
1345 1348 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1346 1349 files)
1347 1350 except PatchError, err:
1348 1351 s = str(err)
1349 1352 if s:
1350 1353 raise util.Abort(s)
1351 1354 else:
1352 1355 raise util.Abort(_('patch failed to apply'))
1353 1356
1354 1357 def b85diff(to, tn):
1355 1358 '''print base85-encoded binary diff'''
1356 1359 def gitindex(text):
1357 1360 if not text:
1358 1361 return '0' * 40
1359 1362 l = len(text)
1360 1363 s = util.sha1('blob %d\0' % l)
1361 1364 s.update(text)
1362 1365 return s.hexdigest()
1363 1366
1364 1367 def fmtline(line):
1365 1368 l = len(line)
1366 1369 if l <= 26:
1367 1370 l = chr(ord('A') + l - 1)
1368 1371 else:
1369 1372 l = chr(l - 26 + ord('a') - 1)
1370 1373 return '%c%s\n' % (l, base85.b85encode(line, True))
1371 1374
1372 1375 def chunk(text, csize=52):
1373 1376 l = len(text)
1374 1377 i = 0
1375 1378 while i < l:
1376 1379 yield text[i:i + csize]
1377 1380 i += csize
1378 1381
1379 1382 tohash = gitindex(to)
1380 1383 tnhash = gitindex(tn)
1381 1384 if tohash == tnhash:
1382 1385 return ""
1383 1386
1384 1387 # TODO: deltas
1385 1388 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1386 1389 (tohash, tnhash, len(tn))]
1387 1390 for l in chunk(zlib.compress(tn)):
1388 1391 ret.append(fmtline(l))
1389 1392 ret.append('\n')
1390 1393 return ''.join(ret)
1391 1394
1392 1395 class GitDiffRequired(Exception):
1393 1396 pass
1394 1397
1395 1398 def diffopts(ui, opts=None, untrusted=False):
1396 1399 def get(key, name=None, getter=ui.configbool):
1397 1400 return ((opts and opts.get(key)) or
1398 1401 getter('diff', name or key, None, untrusted=untrusted))
1399 1402 return mdiff.diffopts(
1400 1403 text=opts and opts.get('text'),
1401 1404 git=get('git'),
1402 1405 nodates=get('nodates'),
1403 1406 showfunc=get('show_function', 'showfunc'),
1404 1407 ignorews=get('ignore_all_space', 'ignorews'),
1405 1408 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1406 1409 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1407 1410 context=get('unified', getter=ui.config))
1408 1411
1409 1412 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1410 1413 losedatafn=None):
1411 1414 '''yields diff of changes to files between two nodes, or node and
1412 1415 working directory.
1413 1416
1414 1417 if node1 is None, use first dirstate parent instead.
1415 1418 if node2 is None, compare node1 with working directory.
1416 1419
1417 1420 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1418 1421 every time some change cannot be represented with the current
1419 1422 patch format. Return False to upgrade to git patch format, True to
1420 1423 accept the loss or raise an exception to abort the diff. It is
1421 1424 called with the name of current file being diffed as 'fn'. If set
1422 1425 to None, patches will always be upgraded to git format when
1423 1426 necessary.
1424 1427 '''
1425 1428
1426 1429 if opts is None:
1427 1430 opts = mdiff.defaultopts
1428 1431
1429 1432 if not node1 and not node2:
1430 1433 node1 = repo.dirstate.parents()[0]
1431 1434
1432 1435 def lrugetfilectx():
1433 1436 cache = {}
1434 1437 order = []
1435 1438 def getfilectx(f, ctx):
1436 1439 fctx = ctx.filectx(f, filelog=cache.get(f))
1437 1440 if f not in cache:
1438 1441 if len(cache) > 20:
1439 1442 del cache[order.pop(0)]
1440 1443 cache[f] = fctx.filelog()
1441 1444 else:
1442 1445 order.remove(f)
1443 1446 order.append(f)
1444 1447 return fctx
1445 1448 return getfilectx
1446 1449 getfilectx = lrugetfilectx()
1447 1450
1448 1451 ctx1 = repo[node1]
1449 1452 ctx2 = repo[node2]
1450 1453
1451 1454 if not changes:
1452 1455 changes = repo.status(ctx1, ctx2, match=match)
1453 1456 modified, added, removed = changes[:3]
1454 1457
1455 1458 if not modified and not added and not removed:
1456 1459 return []
1457 1460
1458 1461 revs = None
1459 1462 if not repo.ui.quiet:
1460 1463 hexfunc = repo.ui.debugflag and hex or short
1461 1464 revs = [hexfunc(node) for node in [node1, node2] if node]
1462 1465
1463 1466 copy = {}
1464 1467 if opts.git or opts.upgrade:
1465 1468 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1466 1469
1467 1470 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1468 1471 modified, added, removed, copy, getfilectx, opts, losedata)
1469 1472 if opts.upgrade and not opts.git:
1470 1473 try:
1471 1474 def losedata(fn):
1472 1475 if not losedatafn or not losedatafn(fn=fn):
1473 1476 raise GitDiffRequired()
1474 1477 # Buffer the whole output until we are sure it can be generated
1475 1478 return list(difffn(opts.copy(git=False), losedata))
1476 1479 except GitDiffRequired:
1477 1480 return difffn(opts.copy(git=True), None)
1478 1481 else:
1479 1482 return difffn(opts, None)
1480 1483
1481 1484 def difflabel(func, *args, **kw):
1482 1485 '''yields 2-tuples of (output, label) based on the output of func()'''
1483 1486 prefixes = [('diff', 'diff.diffline'),
1484 1487 ('copy', 'diff.extended'),
1485 1488 ('rename', 'diff.extended'),
1486 1489 ('old', 'diff.extended'),
1487 1490 ('new', 'diff.extended'),
1488 1491 ('deleted', 'diff.extended'),
1489 1492 ('---', 'diff.file_a'),
1490 1493 ('+++', 'diff.file_b'),
1491 1494 ('@@', 'diff.hunk'),
1492 1495 ('-', 'diff.deleted'),
1493 1496 ('+', 'diff.inserted')]
1494 1497
1495 1498 for chunk in func(*args, **kw):
1496 1499 lines = chunk.split('\n')
1497 1500 for i, line in enumerate(lines):
1498 1501 if i != 0:
1499 1502 yield ('\n', '')
1500 1503 stripline = line
1501 1504 if line and line[0] in '+-':
1502 1505 # highlight trailing whitespace, but only in changed lines
1503 1506 stripline = line.rstrip()
1504 1507 for prefix, label in prefixes:
1505 1508 if stripline.startswith(prefix):
1506 1509 yield (stripline, label)
1507 1510 break
1508 1511 else:
1509 1512 yield (line, '')
1510 1513 if line != stripline:
1511 1514 yield (line[len(stripline):], 'diff.trailingwhitespace')
1512 1515
1513 1516 def diffui(*args, **kw):
1514 1517 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1515 1518 return difflabel(diff, *args, **kw)
1516 1519
1517 1520
1518 1521 def _addmodehdr(header, omode, nmode):
1519 1522 if omode != nmode:
1520 1523 header.append('old mode %s\n' % omode)
1521 1524 header.append('new mode %s\n' % nmode)
1522 1525
1523 1526 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1524 1527 copy, getfilectx, opts, losedatafn):
1525 1528
1526 1529 date1 = util.datestr(ctx1.date())
1527 1530 man1 = ctx1.manifest()
1528 1531
1529 1532 gone = set()
1530 1533 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1531 1534
1532 1535 copyto = dict([(v, k) for k, v in copy.items()])
1533 1536
1534 1537 if opts.git:
1535 1538 revs = None
1536 1539
1537 1540 for f in sorted(modified + added + removed):
1538 1541 to = None
1539 1542 tn = None
1540 1543 dodiff = True
1541 1544 header = []
1542 1545 if f in man1:
1543 1546 to = getfilectx(f, ctx1).data()
1544 1547 if f not in removed:
1545 1548 tn = getfilectx(f, ctx2).data()
1546 1549 a, b = f, f
1547 1550 if opts.git or losedatafn:
1548 1551 if f in added:
1549 1552 mode = gitmode[ctx2.flags(f)]
1550 1553 if f in copy or f in copyto:
1551 1554 if opts.git:
1552 1555 if f in copy:
1553 1556 a = copy[f]
1554 1557 else:
1555 1558 a = copyto[f]
1556 1559 omode = gitmode[man1.flags(a)]
1557 1560 _addmodehdr(header, omode, mode)
1558 1561 if a in removed and a not in gone:
1559 1562 op = 'rename'
1560 1563 gone.add(a)
1561 1564 else:
1562 1565 op = 'copy'
1563 1566 header.append('%s from %s\n' % (op, a))
1564 1567 header.append('%s to %s\n' % (op, f))
1565 1568 to = getfilectx(a, ctx1).data()
1566 1569 else:
1567 1570 losedatafn(f)
1568 1571 else:
1569 1572 if opts.git:
1570 1573 header.append('new file mode %s\n' % mode)
1571 1574 elif ctx2.flags(f):
1572 1575 losedatafn(f)
1573 1576 if util.binary(tn):
1574 1577 if opts.git:
1575 1578 dodiff = 'binary'
1576 1579 else:
1577 1580 losedatafn(f)
1578 1581 if not opts.git and not tn:
1579 1582 # regular diffs cannot represent new empty file
1580 1583 losedatafn(f)
1581 1584 elif f in removed:
1582 1585 if opts.git:
1583 1586 # have we already reported a copy above?
1584 1587 if ((f in copy and copy[f] in added
1585 1588 and copyto[copy[f]] == f) or
1586 1589 (f in copyto and copyto[f] in added
1587 1590 and copy[copyto[f]] == f)):
1588 1591 dodiff = False
1589 1592 else:
1590 1593 header.append('deleted file mode %s\n' %
1591 1594 gitmode[man1.flags(f)])
1592 1595 elif not to:
1593 1596 # regular diffs cannot represent empty file deletion
1594 1597 losedatafn(f)
1595 1598 else:
1596 1599 oflag = man1.flags(f)
1597 1600 nflag = ctx2.flags(f)
1598 1601 binary = util.binary(to) or util.binary(tn)
1599 1602 if opts.git:
1600 1603 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1601 1604 if binary:
1602 1605 dodiff = 'binary'
1603 1606 elif binary or nflag != oflag:
1604 1607 losedatafn(f)
1605 1608 if opts.git:
1606 1609 header.insert(0, mdiff.diffline(revs, a, b, opts))
1607 1610
1608 1611 if dodiff:
1609 1612 if dodiff == 'binary':
1610 1613 text = b85diff(to, tn)
1611 1614 else:
1612 1615 text = mdiff.unidiff(to, date1,
1613 1616 # ctx2 date may be dynamic
1614 1617 tn, util.datestr(ctx2.date()),
1615 1618 a, b, revs, opts=opts)
1616 1619 if header and (text or len(header) > 1):
1617 1620 yield ''.join(header)
1618 1621 if text:
1619 1622 yield text
1620 1623
1621 1624 def diffstatdata(lines):
1622 1625 filename, adds, removes = None, 0, 0
1623 1626 for line in lines:
1624 1627 if line.startswith('diff'):
1625 1628 if filename:
1626 1629 isbinary = adds == 0 and removes == 0
1627 1630 yield (filename, adds, removes, isbinary)
1628 1631 # set numbers to 0 anyway when starting new file
1629 1632 adds, removes = 0, 0
1630 1633 if line.startswith('diff --git'):
1631 1634 filename = gitre.search(line).group(1)
1632 1635 else:
1633 1636 # format: "diff -r ... -r ... filename"
1634 1637 filename = line.split(None, 5)[-1]
1635 1638 elif line.startswith('+') and not line.startswith('+++'):
1636 1639 adds += 1
1637 1640 elif line.startswith('-') and not line.startswith('---'):
1638 1641 removes += 1
1639 1642 if filename:
1640 1643 isbinary = adds == 0 and removes == 0
1641 1644 yield (filename, adds, removes, isbinary)
1642 1645
1643 1646 def diffstat(lines, width=80, git=False):
1644 1647 output = []
1645 1648 stats = list(diffstatdata(lines))
1646 1649
1647 1650 maxtotal, maxname = 0, 0
1648 1651 totaladds, totalremoves = 0, 0
1649 1652 hasbinary = False
1650 1653 for filename, adds, removes, isbinary in stats:
1651 1654 totaladds += adds
1652 1655 totalremoves += removes
1653 1656 maxname = max(maxname, len(filename))
1654 1657 maxtotal = max(maxtotal, adds + removes)
1655 1658 if isbinary:
1656 1659 hasbinary = True
1657 1660
1658 1661 countwidth = len(str(maxtotal))
1659 1662 if hasbinary and countwidth < 3:
1660 1663 countwidth = 3
1661 1664 graphwidth = width - countwidth - maxname - 6
1662 1665 if graphwidth < 10:
1663 1666 graphwidth = 10
1664 1667
1665 1668 def scale(i):
1666 1669 if maxtotal <= graphwidth:
1667 1670 return i
1668 1671 # If diffstat runs out of room it doesn't print anything,
1669 1672 # which isn't very useful, so always print at least one + or -
1670 1673 # if there were at least some changes.
1671 1674 return max(i * graphwidth // maxtotal, int(bool(i)))
1672 1675
1673 1676 for filename, adds, removes, isbinary in stats:
1674 1677 if git and isbinary:
1675 1678 count = 'Bin'
1676 1679 else:
1677 1680 count = adds + removes
1678 1681 pluses = '+' * scale(adds)
1679 1682 minuses = '-' * scale(removes)
1680 1683 output.append(' %-*s | %*s %s%s\n' % (maxname, filename, countwidth,
1681 1684 count, pluses, minuses))
1682 1685
1683 1686 if stats:
1684 1687 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1685 1688 % (len(stats), totaladds, totalremoves))
1686 1689
1687 1690 return ''.join(output)
1688 1691
1689 1692 def diffstatui(*args, **kw):
1690 1693 '''like diffstat(), but yields 2-tuples of (output, label) for
1691 1694 ui.write()
1692 1695 '''
1693 1696
1694 1697 for line in diffstat(*args, **kw).splitlines():
1695 1698 if line and line[-1] in '+-':
1696 1699 name, graph = line.rsplit(' ', 1)
1697 1700 yield (name + ' ', '')
1698 1701 m = re.search(r'\++', graph)
1699 1702 if m:
1700 1703 yield (m.group(0), 'diffstat.inserted')
1701 1704 m = re.search(r'-+', graph)
1702 1705 if m:
1703 1706 yield (m.group(0), 'diffstat.deleted')
1704 1707 else:
1705 1708 yield (line, '')
1706 1709 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now