Mercurial > public > mercurial-scm > hg-stable
comparison mercurial/patch.py @ 4897:4574925db5c0
Add Chris Mason's mpatch library.
The original repo is http://oss.oracle.com/mercurial/mason/mpatch
author | Bryan O'Sullivan <bos@serpentine.com> |
---|---|
date | Tue, 17 Jul 2007 09:39:30 -0700 |
parents | e321f16f4eac |
children | bc905a6c0e76 |
comparison
equal
deleted
inserted
replaced
4896:ee04732fe61d | 4897:4574925db5c0 |
---|---|
1 # patch.py - patch file parsing routines | 1 # patch.py - patch file parsing routines |
2 # | 2 # |
3 # Copyright 2006 Brendan Cully <brendan@kublai.com> | 3 # Copyright 2006 Brendan Cully <brendan@kublai.com> |
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com> | |
4 # | 5 # |
5 # This software may be used and distributed according to the terms | 6 # This software may be used and distributed according to the terms |
6 # of the GNU General Public License, incorporated herein by reference. | 7 # of the GNU General Public License, incorporated herein by reference. |
7 | 8 |
8 from i18n import _ | 9 from i18n import _ |
9 from node import * | 10 from node import * |
10 import base85, cmdutil, mdiff, util, context, revlog | 11 import base85, cmdutil, mdiff, util, context, revlog, diffhelpers |
11 import cStringIO, email.Parser, os, popen2, re, sha | 12 import cStringIO, email.Parser, os, popen2, re, sha |
12 import sys, tempfile, zlib | 13 import sys, tempfile, zlib |
14 | |
15 class PatchError(Exception): | |
16 pass | |
13 | 17 |
14 # helper functions | 18 # helper functions |
15 | 19 |
16 def copyfile(src, dst, basedir=None): | 20 def copyfile(src, dst, basedir=None): |
17 if not basedir: | 21 if not basedir: |
133 | 137 |
134 GP_PATCH = 1 << 0 # we have to run patch | 138 GP_PATCH = 1 << 0 # we have to run patch |
135 GP_FILTER = 1 << 1 # there's some copy/rename operation | 139 GP_FILTER = 1 << 1 # there's some copy/rename operation |
136 GP_BINARY = 1 << 2 # there's a binary patch | 140 GP_BINARY = 1 << 2 # there's a binary patch |
137 | 141 |
138 def readgitpatch(patchname): | 142 def readgitpatch(fp, firstline): |
139 """extract git-style metadata about patches from <patchname>""" | 143 """extract git-style metadata about patches from <patchname>""" |
140 class gitpatch: | 144 class gitpatch: |
141 "op is one of ADD, DELETE, RENAME, MODIFY or COPY" | 145 "op is one of ADD, DELETE, RENAME, MODIFY or COPY" |
142 def __init__(self, path): | 146 def __init__(self, path): |
143 self.path = path | 147 self.path = path |
146 self.op = 'MODIFY' | 150 self.op = 'MODIFY' |
147 self.copymod = False | 151 self.copymod = False |
148 self.lineno = 0 | 152 self.lineno = 0 |
149 self.binary = False | 153 self.binary = False |
150 | 154 |
155 def reader(fp, firstline): | |
156 yield firstline | |
157 for line in fp: | |
158 yield line | |
159 | |
151 # Filter patch for git information | 160 # Filter patch for git information |
152 gitre = re.compile('diff --git a/(.*) b/(.*)') | 161 gitre = re.compile('diff --git a/(.*) b/(.*)') |
153 pf = file(patchname) | |
154 gp = None | 162 gp = None |
155 gitpatches = [] | 163 gitpatches = [] |
156 # Can have a git patch with only metadata, causing patch to complain | 164 # Can have a git patch with only metadata, causing patch to complain |
157 dopatch = 0 | 165 dopatch = 0 |
158 | 166 |
159 lineno = 0 | 167 lineno = 0 |
160 for line in pf: | 168 for line in reader(fp, firstline): |
161 lineno += 1 | 169 lineno += 1 |
162 if line.startswith('diff --git'): | 170 if line.startswith('diff --git'): |
163 m = gitre.match(line) | 171 m = gitre.match(line) |
164 if m: | 172 if m: |
165 if gp: | 173 if gp: |
202 if not gitpatches: | 210 if not gitpatches: |
203 dopatch = GP_PATCH | 211 dopatch = GP_PATCH |
204 | 212 |
205 return (dopatch, gitpatches) | 213 return (dopatch, gitpatches) |
206 | 214 |
207 def dogitpatch(patchname, gitpatches, cwd=None): | 215 def patch(patchname, ui, strip=1, cwd=None, files={}): |
208 """Preprocess git patch so that vanilla patch can handle it""" | 216 """apply the patch <patchname> to the working directory. |
209 def extractbin(fp): | 217 a list of patched files is returned""" |
210 i = [0] # yuck | 218 fp = file(patchname) |
211 def readline(): | 219 fuzz = False |
212 i[0] += 1 | 220 if cwd: |
213 return fp.readline().rstrip() | 221 curdir = os.getcwd() |
214 line = readline() | 222 os.chdir(cwd) |
223 try: | |
224 ret = applydiff(ui, fp, files, strip=strip) | |
225 except PatchError: | |
226 raise util.Abort(_("patch failed to apply")) | |
227 if cwd: | |
228 os.chdir(curdir) | |
229 if ret < 0: | |
230 raise util.Abort(_("patch failed to apply")) | |
231 if ret > 0: | |
232 fuzz = True | |
233 return fuzz | |
234 | |
235 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1 | |
236 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@') | |
237 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)') | |
238 | |
239 class patchfile: | |
240 def __init__(self, ui, fname): | |
241 self.fname = fname | |
242 self.ui = ui | |
243 try: | |
244 fp = file(fname, 'r') | |
245 self.lines = fp.readlines() | |
246 self.exists = True | |
247 except IOError: | |
248 dirname = os.path.dirname(fname) | |
249 if dirname and not os.path.isdir(dirname): | |
250 dirs = dirname.split(os.path.sep) | |
251 d = "" | |
252 for x in dirs: | |
253 d = os.path.join(d, x) | |
254 if not os.path.isdir(d): | |
255 os.mkdir(d) | |
256 self.lines = [] | |
257 self.exists = False | |
258 | |
259 self.hash = {} | |
260 self.dirty = 0 | |
261 self.offset = 0 | |
262 self.rej = [] | |
263 self.fileprinted = False | |
264 self.printfile(False) | |
265 self.hunks = 0 | |
266 | |
267 def printfile(self, warn): | |
268 if self.fileprinted: | |
269 return | |
270 if warn or self.ui.verbose: | |
271 self.fileprinted = True | |
272 s = _("patching file %s\n" % self.fname) | |
273 if warn: | |
274 self.ui.warn(s) | |
275 else: | |
276 self.ui.note(s) | |
277 | |
278 | |
279 def findlines(self, l, linenum): | |
280 # looks through the hash and finds candidate lines. The | |
281 # result is a list of line numbers sorted based on distance | |
282 # from linenum | |
283 def sorter(a, b): | |
284 vala = abs(a - linenum) | |
285 valb = abs(b - linenum) | |
286 return cmp(vala, valb) | |
287 | |
288 try: | |
289 cand = self.hash[l] | |
290 except: | |
291 return [] | |
292 | |
293 if len(cand) > 1: | |
294 # resort our list of potentials forward then back. | |
295 cand.sort(cmp=sorter) | |
296 return cand | |
297 | |
298 def hashlines(self): | |
299 self.hash = {} | |
300 for x in xrange(len(self.lines)): | |
301 s = self.lines[x] | |
302 self.hash.setdefault(s, []).append(x) | |
303 | |
304 def write_rej(self): | |
305 # our rejects are a little different from patch(1). This always | |
306 # creates rejects in the same form as the original patch. A file | |
307 # header is inserted so that you can run the reject through patch again | |
308 # without having to type the filename. | |
309 | |
310 if not self.rej: | |
311 return | |
312 if self.hunks != 1: | |
313 hunkstr = "s" | |
314 else: | |
315 hunkstr = "" | |
316 | |
317 fname = self.fname + ".rej" | |
318 self.ui.warn( | |
319 _("%d out of %d hunk%s FAILED -- saving rejects to file %s\n" % | |
320 (len(self.rej), self.hunks, hunkstr, fname))) | |
321 try: os.unlink(fname) | |
322 except: | |
323 pass | |
324 fp = file(fname, 'w') | |
325 base = os.path.basename(self.fname) | |
326 fp.write("--- %s\n+++ %s\n" % (base, base)) | |
327 for x in self.rej: | |
328 for l in x.hunk: | |
329 fp.write(l) | |
330 if l[-1] != '\n': | |
331 fp.write("\n\ No newline at end of file\n") | |
332 | |
333 def write(self, dest=None): | |
334 if self.dirty: | |
335 if not dest: | |
336 dest = self.fname | |
337 st = None | |
338 try: | |
339 st = os.lstat(dest) | |
340 if st.st_nlink > 1: | |
341 os.unlink(dest) | |
342 except: pass | |
343 fp = file(dest, 'w') | |
344 if st: | |
345 os.chmod(dest, st.st_mode) | |
346 fp.writelines(self.lines) | |
347 fp.close() | |
348 | |
349 def close(self): | |
350 self.write() | |
351 self.write_rej() | |
352 | |
353 def apply(self, h, reverse): | |
354 if not h.complete(): | |
355 raise PatchError("bad hunk #%d %s (%d %d %d %d)" % | |
356 (h.number, h.desc, len(h.a), h.lena, len(h.b), | |
357 h.lenb)) | |
358 | |
359 self.hunks += 1 | |
360 if reverse: | |
361 h.reverse() | |
362 | |
363 if self.exists and h.createfile(): | |
364 self.ui.warn(_("file %s already exists\n" % self.fname)) | |
365 self.rej.append(h) | |
366 return -1 | |
367 | |
368 if isinstance(h, binhunk): | |
369 if h.rmfile(): | |
370 os.unlink(self.fname) | |
371 else: | |
372 self.lines[:] = h.new() | |
373 self.offset += len(h.new()) | |
374 self.dirty = 1 | |
375 return 0 | |
376 | |
377 # fast case first, no offsets, no fuzz | |
378 old = h.old() | |
379 # patch starts counting at 1 unless we are adding the file | |
380 if h.starta == 0: | |
381 start = 0 | |
382 else: | |
383 start = h.starta + self.offset - 1 | |
384 orig_start = start | |
385 if diffhelpers.testhunk(old, self.lines, start) == 0: | |
386 if h.rmfile(): | |
387 os.unlink(self.fname) | |
388 else: | |
389 self.lines[start : start + h.lena] = h.new() | |
390 self.offset += h.lenb - h.lena | |
391 self.dirty = 1 | |
392 return 0 | |
393 | |
394 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it | |
395 self.hashlines() | |
396 if h.hunk[-1][0] != ' ': | |
397 # if the hunk tried to put something at the bottom of the file | |
398 # override the start line and use eof here | |
399 search_start = len(self.lines) | |
400 else: | |
401 search_start = orig_start | |
402 | |
403 for fuzzlen in xrange(3): | |
404 for toponly in [ True, False ]: | |
405 old = h.old(fuzzlen, toponly) | |
406 | |
407 cand = self.findlines(old[0][1:], search_start) | |
408 for l in cand: | |
409 if diffhelpers.testhunk(old, self.lines, l) == 0: | |
410 newlines = h.new(fuzzlen, toponly) | |
411 self.lines[l : l + len(old)] = newlines | |
412 self.offset += len(newlines) - len(old) | |
413 self.dirty = 1 | |
414 if fuzzlen: | |
415 fuzzstr = "with fuzz %d " % fuzzlen | |
416 f = self.ui.warn | |
417 self.printfile(True) | |
418 else: | |
419 fuzzstr = "" | |
420 f = self.ui.note | |
421 offset = l - orig_start - fuzzlen | |
422 if offset == 1: | |
423 linestr = "line" | |
424 else: | |
425 linestr = "lines" | |
426 f(_("Hunk #%d succeeded at %d %s(offset %d %s).\n" % | |
427 (h.number, l+1, fuzzstr, offset, linestr))) | |
428 return fuzzlen | |
429 self.printfile(True) | |
430 self.ui.warn(_("Hunk #%d FAILED at %d\n" % (h.number, orig_start))) | |
431 self.rej.append(h) | |
432 return -1 | |
433 | |
434 class hunk: | |
435 def __init__(self, desc, num, lr, context): | |
436 self.number = num | |
437 self.desc = desc | |
438 self.hunk = [ desc ] | |
439 self.a = [] | |
440 self.b = [] | |
441 if context: | |
442 self.read_context_hunk(lr) | |
443 else: | |
444 self.read_unified_hunk(lr) | |
445 | |
446 def read_unified_hunk(self, lr): | |
447 m = unidesc.match(self.desc) | |
448 if not m: | |
449 raise PatchError("bad hunk #%d" % self.number) | |
450 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups() | |
451 if self.lena == None: | |
452 self.lena = 1 | |
453 else: | |
454 self.lena = int(self.lena) | |
455 if self.lenb == None: | |
456 self.lenb = 1 | |
457 else: | |
458 self.lenb = int(self.lenb) | |
459 self.starta = int(self.starta) | |
460 self.startb = int(self.startb) | |
461 diffhelpers.addlines(lr.fp, self.hunk, self.lena, self.lenb, self.a, self.b) | |
462 # if we hit eof before finishing out the hunk, the last line will | |
463 # be zero length. Lets try to fix it up. | |
464 while len(self.hunk[-1]) == 0: | |
465 del self.hunk[-1] | |
466 del self.a[-1] | |
467 del self.b[-1] | |
468 self.lena -= 1 | |
469 self.lenb -= 1 | |
470 | |
471 def read_context_hunk(self, lr): | |
472 self.desc = lr.readline() | |
473 m = contextdesc.match(self.desc) | |
474 if not m: | |
475 raise PatchError("bad hunk #%d" % self.number) | |
476 foo, self.starta, foo2, aend, foo3 = m.groups() | |
477 self.starta = int(self.starta) | |
478 if aend == None: | |
479 aend = self.starta | |
480 self.lena = int(aend) - self.starta | |
481 if self.starta: | |
482 self.lena += 1 | |
483 for x in xrange(self.lena): | |
484 l = lr.readline() | |
485 if l.startswith('---'): | |
486 lr.push(l) | |
487 break | |
488 s = l[2:] | |
489 if l.startswith('- ') or l.startswith('! '): | |
490 u = '-' + s | |
491 elif l.startswith(' '): | |
492 u = ' ' + s | |
493 else: | |
494 raise PatchError("bad hunk #%d old text line %d" % (self.number, x)) | |
495 self.a.append(u) | |
496 self.hunk.append(u) | |
497 | |
498 l = lr.readline() | |
499 if l.startswith('\ '): | |
500 s = self.a[-1][:-1] | |
501 self.a[-1] = s | |
502 self.hunk[-1] = s | |
503 l = lr.readline() | |
504 m = contextdesc.match(l) | |
505 if not m: | |
506 raise PatchError("bad hunk #%d" % self.number) | |
507 foo, self.startb, foo2, bend, foo3 = m.groups() | |
508 self.startb = int(self.startb) | |
509 if bend == None: | |
510 bend = self.startb | |
511 self.lenb = int(bend) - self.startb | |
512 if self.startb: | |
513 self.lenb += 1 | |
514 hunki = 1 | |
515 for x in xrange(self.lenb): | |
516 l = lr.readline() | |
517 if l.startswith('\ '): | |
518 s = self.b[-1][:-1] | |
519 self.b[-1] = s | |
520 self.hunk[hunki-1] = s | |
521 continue | |
522 if not l: | |
523 lr.push(l) | |
524 break | |
525 s = l[2:] | |
526 if l.startswith('+ ') or l.startswith('! '): | |
527 u = '+' + s | |
528 elif l.startswith(' '): | |
529 u = ' ' + s | |
530 elif len(self.b) == 0: | |
531 # this can happen when the hunk does not add any lines | |
532 lr.push(l) | |
533 break | |
534 else: | |
535 raise PatchError("bad hunk #%d old text line %d" % (self.number, x)) | |
536 self.b.append(s) | |
537 while True: | |
538 if hunki >= len(self.hunk): | |
539 h = "" | |
540 else: | |
541 h = self.hunk[hunki] | |
542 hunki += 1 | |
543 if h == u: | |
544 break | |
545 elif h.startswith('-'): | |
546 continue | |
547 else: | |
548 self.hunk.insert(hunki-1, u) | |
549 break | |
550 | |
551 if not self.a: | |
552 # this happens when lines were only added to the hunk | |
553 for x in self.hunk: | |
554 if x.startswith('-') or x.startswith(' '): | |
555 self.a.append(x) | |
556 if not self.b: | |
557 # this happens when lines were only deleted from the hunk | |
558 for x in self.hunk: | |
559 if x.startswith('+') or x.startswith(' '): | |
560 self.b.append(x[1:]) | |
561 # @@ -start,len +start,len @@ | |
562 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena, | |
563 self.startb, self.lenb) | |
564 self.hunk[0] = self.desc | |
565 | |
566 def reverse(self): | |
567 origlena = self.lena | |
568 origstarta = self.starta | |
569 self.lena = self.lenb | |
570 self.starta = self.startb | |
571 self.lenb = origlena | |
572 self.startb = origstarta | |
573 self.a = [] | |
574 self.b = [] | |
575 # self.hunk[0] is the @@ description | |
576 for x in xrange(1, len(self.hunk)): | |
577 o = self.hunk[x] | |
578 if o.startswith('-'): | |
579 n = '+' + o[1:] | |
580 self.b.append(o[1:]) | |
581 elif o.startswith('+'): | |
582 n = '-' + o[1:] | |
583 self.a.append(n) | |
584 else: | |
585 n = o | |
586 self.b.append(o[1:]) | |
587 self.a.append(o) | |
588 self.hunk[x] = o | |
589 | |
590 def fix_newline(self): | |
591 diffhelpers.fix_newline(self.hunk, self.a, self.b) | |
592 | |
593 def complete(self): | |
594 return len(self.a) == self.lena and len(self.b) == self.lenb | |
595 | |
596 def createfile(self): | |
597 return self.starta == 0 and self.lena == 0 | |
598 | |
599 def rmfile(self): | |
600 return self.startb == 0 and self.lenb == 0 | |
601 | |
602 def fuzzit(self, l, fuzz, toponly): | |
603 # this removes context lines from the top and bottom of list 'l'. It | |
604 # checks the hunk to make sure only context lines are removed, and then | |
605 # returns a new shortened list of lines. | |
606 fuzz = min(fuzz, len(l)-1) | |
607 if fuzz: | |
608 top = 0 | |
609 bot = 0 | |
610 hlen = len(self.hunk) | |
611 for x in xrange(hlen-1): | |
612 # the hunk starts with the @@ line, so use x+1 | |
613 if self.hunk[x+1][0] == ' ': | |
614 top += 1 | |
615 else: | |
616 break | |
617 if not toponly: | |
618 for x in xrange(hlen-1): | |
619 if self.hunk[hlen-bot-1][0] == ' ': | |
620 bot += 1 | |
621 else: | |
622 break | |
623 | |
624 # top and bot now count context in the hunk | |
625 # adjust them if either one is short | |
626 context = max(top, bot, 3) | |
627 if bot < context: | |
628 bot = max(0, fuzz - (context - bot)) | |
629 else: | |
630 bot = min(fuzz, bot) | |
631 if top < context: | |
632 top = max(0, fuzz - (context - top)) | |
633 else: | |
634 top = min(fuzz, top) | |
635 | |
636 return l[top:len(l)-bot] | |
637 return l | |
638 | |
639 def old(self, fuzz=0, toponly=False): | |
640 return self.fuzzit(self.a, fuzz, toponly) | |
641 | |
642 def newctrl(self): | |
643 res = [] | |
644 for x in self.hunk: | |
645 c = x[0] | |
646 if c == ' ' or c == '+': | |
647 res.append(x) | |
648 return res | |
649 | |
650 def new(self, fuzz=0, toponly=False): | |
651 return self.fuzzit(self.b, fuzz, toponly) | |
652 | |
653 class binhunk: | |
654 'A binary patch file. Only understands literals so far.' | |
655 def __init__(self, gitpatch): | |
656 self.gitpatch = gitpatch | |
657 self.text = None | |
658 self.hunk = ['GIT binary patch\n'] | |
659 | |
660 def createfile(self): | |
661 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY') | |
662 | |
663 def rmfile(self): | |
664 return self.gitpatch.op == 'DELETE' | |
665 | |
666 def complete(self): | |
667 return self.text is not None | |
668 | |
669 def new(self): | |
670 return [self.text] | |
671 | |
672 def extract(self, fp): | |
673 line = fp.readline() | |
674 self.hunk.append(line) | |
215 while line and not line.startswith('literal '): | 675 while line and not line.startswith('literal '): |
216 line = readline() | 676 line = fp.readline() |
677 self.hunk.append(line) | |
217 if not line: | 678 if not line: |
218 return None, i[0] | 679 raise PatchError('could not extract binary patch') |
219 size = int(line[8:]) | 680 size = int(line[8:].rstrip()) |
220 dec = [] | 681 dec = [] |
221 line = readline() | 682 line = fp.readline() |
222 while line: | 683 self.hunk.append(line) |
684 while len(line) > 1: | |
223 l = line[0] | 685 l = line[0] |
224 if l <= 'Z' and l >= 'A': | 686 if l <= 'Z' and l >= 'A': |
225 l = ord(l) - ord('A') + 1 | 687 l = ord(l) - ord('A') + 1 |
226 else: | 688 else: |
227 l = ord(l) - ord('a') + 27 | 689 l = ord(l) - ord('a') + 27 |
228 dec.append(base85.b85decode(line[1:])[:l]) | 690 dec.append(base85.b85decode(line[1:-1])[:l]) |
229 line = readline() | 691 line = fp.readline() |
692 self.hunk.append(line) | |
230 text = zlib.decompress(''.join(dec)) | 693 text = zlib.decompress(''.join(dec)) |
231 if len(text) != size: | 694 if len(text) != size: |
232 raise util.Abort(_('binary patch is %d bytes, not %d') % | 695 raise PatchError('binary patch is %d bytes, not %d' % |
233 (len(text), size)) | 696 len(text), size) |
234 return text, i[0] | 697 self.text = text |
235 | 698 |
236 pf = file(patchname) | 699 def parsefilename(str): |
237 pfline = 1 | 700 # --- filename \t|space stuff |
238 | 701 s = str[4:] |
239 fd, patchname = tempfile.mkstemp(prefix='hg-patch-') | 702 i = s.find('\t') |
240 tmpfp = os.fdopen(fd, 'w') | 703 if i < 0: |
241 | 704 i = s.find(' ') |
242 try: | 705 if i < 0: |
243 for i in xrange(len(gitpatches)): | 706 return s |
244 p = gitpatches[i] | 707 return s[:i] |
245 if not p.copymod and not p.binary: | 708 |
709 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse): | |
710 def pathstrip(path, count=1): | |
711 pathlen = len(path) | |
712 i = 0 | |
713 if count == 0: | |
714 return path.rstrip() | |
715 while count > 0: | |
716 i = path.find(os.sep, i) | |
717 if i == -1: | |
718 raise PatchError("Unable to strip away %d dirs from %s" % | |
719 (count, path)) | |
720 i += 1 | |
721 # consume '//' in the path | |
722 while i < pathlen - 1 and path[i] == os.sep: | |
723 i += 1 | |
724 count -= 1 | |
725 return path[i:].rstrip() | |
726 | |
727 nulla = afile_orig == "/dev/null" | |
728 nullb = bfile_orig == "/dev/null" | |
729 afile = pathstrip(afile_orig, strip) | |
730 gooda = os.path.exists(afile) and not nulla | |
731 bfile = pathstrip(bfile_orig, strip) | |
732 if afile == bfile: | |
733 goodb = gooda | |
734 else: | |
735 goodb = os.path.exists(bfile) and not nullb | |
736 createfunc = hunk.createfile | |
737 if reverse: | |
738 createfunc = hunk.rmfile | |
739 if not goodb and not gooda and not createfunc(): | |
740 raise PatchError(_("Unable to find %s or %s for patching\n" % | |
741 (afile, bfile))) | |
742 if gooda and goodb: | |
743 fname = bfile | |
744 if afile in bfile: | |
745 fname = afile | |
746 elif gooda: | |
747 fname = afile | |
748 elif not nullb: | |
749 fname = bfile | |
750 if afile in bfile: | |
751 fname = afile | |
752 elif not nulla: | |
753 fname = afile | |
754 return fname | |
755 | |
756 class linereader: | |
757 # simple class to allow pushing lines back into the input stream | |
758 def __init__(self, fp): | |
759 self.fp = fp | |
760 self.buf = [] | |
761 | |
762 def push(self, line): | |
763 self.buf.append(line) | |
764 | |
765 def readline(self): | |
766 if self.buf: | |
767 l = self.buf[0] | |
768 del self.buf[0] | |
769 return l | |
770 return self.fp.readline() | |
771 | |
772 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False, | |
773 rejmerge=None, updatedir=None): | |
774 """reads a patch from fp and tries to apply it. The dict 'changed' is | |
775 filled in with all of the filenames changed by the patch. Returns 0 | |
776 for a clean patch, -1 if any rejects were found and 1 if there was | |
777 any fuzz.""" | |
778 | |
779 def scangitpatch(fp, firstline, cwd=None): | |
780 '''git patches can modify a file, then copy that file to | |
781 a new file, but expect the source to be the unmodified form. | |
782 So we scan the patch looking for that case so we can do | |
783 the copies ahead of time.''' | |
784 | |
785 pos = 0 | |
786 try: | |
787 pos = fp.tell() | |
788 except IOError: | |
789 fp = cStringIO.StringIO(fp.read()) | |
790 | |
791 (dopatch, gitpatches) = readgitpatch(fp, firstline) | |
792 for gp in gitpatches: | |
793 if gp.copymod: | |
794 copyfile(gp.oldpath, gp.path, basedir=cwd) | |
795 | |
796 fp.seek(pos) | |
797 | |
798 return fp, dopatch, gitpatches | |
799 | |
800 current_hunk = None | |
801 current_file = None | |
802 afile = "" | |
803 bfile = "" | |
804 state = None | |
805 hunknum = 0 | |
806 rejects = 0 | |
807 | |
808 git = False | |
809 gitre = re.compile('diff --git (a/.*) (b/.*)') | |
810 | |
811 # our states | |
812 BFILE = 1 | |
813 err = 0 | |
814 context = None | |
815 lr = linereader(fp) | |
816 dopatch = True | |
817 gitworkdone = False | |
818 | |
819 while True: | |
820 newfile = False | |
821 x = lr.readline() | |
822 if not x: | |
823 break | |
824 if current_hunk: | |
825 if x.startswith('\ '): | |
826 current_hunk.fix_newline() | |
827 ret = current_file.apply(current_hunk, reverse) | |
828 if ret > 0: | |
829 err = 1 | |
830 current_hunk = None | |
831 gitworkdone = False | |
832 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or | |
833 ((context or context == None) and x.startswith('***************')))): | |
834 try: | |
835 if context == None and x.startswith('***************'): | |
836 context = True | |
837 current_hunk = hunk(x, hunknum + 1, lr, context) | |
838 except PatchError: | |
839 current_hunk = None | |
246 continue | 840 continue |
247 | 841 hunknum += 1 |
248 # rewrite patch hunk | 842 if not current_file: |
249 while pfline < p.lineno: | 843 if sourcefile: |
250 tmpfp.write(pf.readline()) | 844 current_file = patchfile(ui, sourcefile) |
251 pfline += 1 | 845 else: |
252 | 846 current_file = selectfile(afile, bfile, current_hunk, |
253 if p.binary: | 847 strip, reverse) |
254 text, delta = extractbin(pf) | 848 current_file = patchfile(ui, current_file) |
255 if not text: | 849 changed.setdefault(current_file.fname, (None, None)) |
256 raise util.Abort(_('binary patch extraction failed')) | 850 elif state == BFILE and x.startswith('GIT binary patch'): |
257 pfline += delta | 851 current_hunk = binhunk(changed[bfile[2:]][1]) |
258 if not cwd: | 852 if not current_file: |
259 cwd = os.getcwd() | 853 if sourcefile: |
260 absdst = os.path.join(cwd, p.path) | 854 current_file = patchfile(ui, sourcefile) |
261 basedir = os.path.dirname(absdst) | 855 else: |
262 if not os.path.isdir(basedir): | 856 current_file = selectfile(afile, bfile, current_hunk, |
263 os.makedirs(basedir) | 857 strip, reverse) |
264 out = file(absdst, 'wb') | 858 current_file = patchfile(ui, current_file) |
265 out.write(text) | 859 hunknum += 1 |
266 out.close() | 860 current_hunk.extract(fp) |
267 elif p.copymod: | 861 elif x.startswith('diff --git'): |
268 copyfile(p.oldpath, p.path, basedir=cwd) | 862 # check for git diff, scanning the whole patch file if needed |
269 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path)) | 863 m = gitre.match(x) |
270 line = pf.readline() | 864 if m: |
271 pfline += 1 | 865 afile, bfile = m.group(1, 2) |
272 while not line.startswith('--- a/'): | 866 if not git: |
273 tmpfp.write(line) | 867 git = True |
274 line = pf.readline() | 868 fp, dopatch, gitpatches = scangitpatch(fp, x) |
275 pfline += 1 | 869 for gp in gitpatches: |
276 tmpfp.write('--- a/%s\n' % p.path) | 870 changed[gp.path] = (gp.op, gp) |
277 | 871 # else error? |
278 line = pf.readline() | 872 # copy/rename + modify should modify target, not source |
279 while line: | 873 if changed.get(bfile[2:], (None, None))[0] in ('COPY', |
280 tmpfp.write(line) | 874 'RENAME'): |
281 line = pf.readline() | 875 afile = bfile |
282 except: | 876 gitworkdone = True |
283 tmpfp.close() | 877 newfile = True |
284 os.unlink(patchname) | 878 elif x.startswith('---'): |
285 raise | 879 # check for a unified diff |
286 | 880 l2 = lr.readline() |
287 tmpfp.close() | 881 if not l2.startswith('+++'): |
288 return patchname | 882 lr.push(l2) |
289 | 883 continue |
290 def patch(patchname, ui, strip=1, cwd=None, files={}): | 884 newfile = True |
291 """apply the patch <patchname> to the working directory. | 885 context = False |
292 a list of patched files is returned""" | 886 afile = parsefilename(x) |
293 | 887 bfile = parsefilename(l2) |
294 # helper function | 888 elif x.startswith('***'): |
295 def __patch(patchname): | 889 # check for a context diff |
296 """patch and updates the files and fuzz variables""" | 890 l2 = lr.readline() |
297 fuzz = False | 891 if not l2.startswith('---'): |
298 | 892 lr.push(l2) |
299 args = [] | 893 continue |
300 patcher = ui.config('ui', 'patch') | 894 l3 = lr.readline() |
301 if not patcher: | 895 lr.push(l3) |
302 patcher = util.find_exe('gpatch') or util.find_exe('patch') | 896 if not l3.startswith("***************"): |
303 # Try to be smart only if patch call was not supplied | 897 lr.push(l2) |
304 if util.needbinarypatch(): | 898 continue |
305 args.append('--binary') | 899 newfile = True |
306 | 900 context = True |
307 if not patcher: | 901 afile = parsefilename(x) |
308 raise util.Abort(_('no patch command found in hgrc or PATH')) | 902 bfile = parsefilename(l2) |
309 | 903 |
310 if cwd: | 904 if newfile: |
311 args.append('-d %s' % util.shellquote(cwd)) | 905 if current_file: |
312 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip, | 906 current_file.close() |
313 util.shellquote(patchname))) | 907 if rejmerge: |
314 | 908 rejmerge(current_file) |
315 for line in fp: | 909 rejects += len(current_file.rej) |
316 line = line.rstrip() | 910 state = BFILE |
317 ui.note(line + '\n') | 911 current_file = None |
318 if line.startswith('patching file '): | 912 hunknum = 0 |
319 pf = util.parse_patch_output(line) | 913 if current_hunk: |
320 printed_file = False | 914 if current_hunk.complete(): |
321 files.setdefault(pf, (None, None)) | 915 ret = current_file.apply(current_hunk, reverse) |
322 elif line.find('with fuzz') >= 0: | 916 if ret > 0: |
323 fuzz = True | 917 err = 1 |
324 if not printed_file: | 918 else: |
325 ui.warn(pf + '\n') | 919 fname = current_file and current_file.fname or None |
326 printed_file = True | 920 raise PatchError("malformed patch %s %s" % (fname, |
327 ui.warn(line + '\n') | 921 current_hunk.desc)) |
328 elif line.find('saving rejects to file') >= 0: | 922 if current_file: |
329 ui.warn(line + '\n') | 923 current_file.close() |
330 elif line.find('FAILED') >= 0: | 924 if rejmerge: |
331 if not printed_file: | 925 rejmerge(current_file) |
332 ui.warn(pf + '\n') | 926 rejects += len(current_file.rej) |
333 printed_file = True | 927 if updatedir and git: |
334 ui.warn(line + '\n') | 928 updatedir(gitpatches) |
335 code = fp.close() | 929 if rejects: |
336 if code: | 930 return -1 |
337 raise util.Abort(_("patch command failed: %s") % | 931 if hunknum == 0 and dopatch and not gitworkdone: |
338 util.explain_exit(code)[0]) | 932 raise PatchError("No valid hunks found") |
339 return fuzz | 933 return err |
340 | |
341 (dopatch, gitpatches) = readgitpatch(patchname) | |
342 for gp in gitpatches: | |
343 files[gp.path] = (gp.op, gp) | |
344 | |
345 fuzz = False | |
346 if dopatch: | |
347 filterpatch = dopatch & (GP_FILTER | GP_BINARY) | |
348 if filterpatch: | |
349 patchname = dogitpatch(patchname, gitpatches, cwd=cwd) | |
350 try: | |
351 if dopatch & GP_PATCH: | |
352 fuzz = __patch(patchname) | |
353 finally: | |
354 if filterpatch: | |
355 os.unlink(patchname) | |
356 | |
357 return fuzz | |
358 | 934 |
359 def diffopts(ui, opts={}, untrusted=False): | 935 def diffopts(ui, opts={}, untrusted=False): |
360 def get(key, name=None): | 936 def get(key, name=None): |
361 return (opts.get(key) or | 937 return (opts.get(key) or |
362 ui.configbool('diff', name or key, None, untrusted=untrusted)) | 938 ui.configbool('diff', name or key, None, untrusted=untrusted)) |