Mercurial > public > mercurial-scm > hg
comparison mercurial/patch.py @ 6042:2da5b19a6460
Merge with crew
author | Bryan O'Sullivan <bos@serpentine.com> |
---|---|
date | Wed, 06 Feb 2008 19:57:52 -0800 |
parents | 1d0bfa4c75c0 d39af2eabb8c |
children | 36ab165abbe2 |
comparison
equal
deleted
inserted
replaced
6041:dd714452c26e | 6042:2da5b19a6460 |
---|---|
7 # of the GNU General Public License, incorporated herein by reference. | 7 # of the GNU General Public License, incorporated herein by reference. |
8 | 8 |
9 from i18n import _ | 9 from i18n import _ |
10 from node import * | 10 from node import * |
11 import base85, cmdutil, mdiff, util, context, revlog, diffhelpers | 11 import base85, cmdutil, mdiff, util, context, revlog, diffhelpers |
12 import cStringIO, email.Parser, os, popen2, re, sha | 12 import cStringIO, email.Parser, os, popen2, re, sha, errno |
13 import sys, tempfile, zlib | 13 import sys, tempfile, zlib |
14 | 14 |
15 class PatchError(Exception): | 15 class PatchError(Exception): |
16 pass | 16 pass |
17 | 17 |
57 try: | 57 try: |
58 msg = email.Parser.Parser().parse(fileobj) | 58 msg = email.Parser.Parser().parse(fileobj) |
59 | 59 |
60 subject = msg['Subject'] | 60 subject = msg['Subject'] |
61 user = msg['From'] | 61 user = msg['From'] |
62 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '') | |
62 # should try to parse msg['Date'] | 63 # should try to parse msg['Date'] |
63 date = None | 64 date = None |
64 nodeid = None | 65 nodeid = None |
65 branch = None | 66 branch = None |
66 parents = [] | 67 parents = [] |
109 branch = line[9:] | 110 branch = line[9:] |
110 elif line.startswith("# Node ID "): | 111 elif line.startswith("# Node ID "): |
111 nodeid = line[10:] | 112 nodeid = line[10:] |
112 elif line.startswith("# Parent "): | 113 elif line.startswith("# Parent "): |
113 parents.append(line[10:]) | 114 parents.append(line[10:]) |
114 elif line == '---' and 'git-send-email' in msg['X-Mailer']: | 115 elif line == '---' and gitsendmail: |
115 ignoretext = True | 116 ignoretext = True |
116 if not line.startswith('# ') and not ignoretext: | 117 if not line.startswith('# ') and not ignoretext: |
117 cfp.write(line) | 118 cfp.write(line) |
118 cfp.write('\n') | 119 cfp.write('\n') |
119 message = cfp.getvalue() | 120 message = cfp.getvalue() |
140 | 141 |
141 GP_PATCH = 1 << 0 # we have to run patch | 142 GP_PATCH = 1 << 0 # we have to run patch |
142 GP_FILTER = 1 << 1 # there's some copy/rename operation | 143 GP_FILTER = 1 << 1 # there's some copy/rename operation |
143 GP_BINARY = 1 << 2 # there's a binary patch | 144 GP_BINARY = 1 << 2 # there's a binary patch |
144 | 145 |
145 def readgitpatch(fp, firstline): | 146 def readgitpatch(fp, firstline=None): |
146 """extract git-style metadata about patches from <patchname>""" | 147 """extract git-style metadata about patches from <patchname>""" |
147 class gitpatch: | 148 class gitpatch: |
148 "op is one of ADD, DELETE, RENAME, MODIFY or COPY" | 149 "op is one of ADD, DELETE, RENAME, MODIFY or COPY" |
149 def __init__(self, path): | 150 def __init__(self, path): |
150 self.path = path | 151 self.path = path |
151 self.oldpath = None | 152 self.oldpath = None |
152 self.mode = None | 153 self.mode = None |
153 self.op = 'MODIFY' | 154 self.op = 'MODIFY' |
154 self.copymod = False | |
155 self.lineno = 0 | 155 self.lineno = 0 |
156 self.binary = False | 156 self.binary = False |
157 | 157 |
158 def reader(fp, firstline): | 158 def reader(fp, firstline): |
159 yield firstline | 159 if firstline is not None: |
160 yield firstline | |
160 for line in fp: | 161 for line in fp: |
161 yield line | 162 yield line |
162 | 163 |
163 # Filter patch for git information | 164 # Filter patch for git information |
164 gitre = re.compile('diff --git a/(.*) b/(.*)') | 165 gitre = re.compile('diff --git a/(.*) b/(.*)') |
179 gp = gitpatch(dst) | 180 gp = gitpatch(dst) |
180 gp.lineno = lineno | 181 gp.lineno = lineno |
181 elif gp: | 182 elif gp: |
182 if line.startswith('--- '): | 183 if line.startswith('--- '): |
183 if gp.op in ('COPY', 'RENAME'): | 184 if gp.op in ('COPY', 'RENAME'): |
184 gp.copymod = True | |
185 dopatch |= GP_FILTER | 185 dopatch |= GP_FILTER |
186 gitpatches.append(gp) | 186 gitpatches.append(gp) |
187 gp = None | 187 gp = None |
188 dopatch |= GP_PATCH | 188 dopatch |= GP_PATCH |
189 continue | 189 continue |
199 gp.path = line[8:].rstrip() | 199 gp.path = line[8:].rstrip() |
200 elif line.startswith('deleted file'): | 200 elif line.startswith('deleted file'): |
201 gp.op = 'DELETE' | 201 gp.op = 'DELETE' |
202 elif line.startswith('new file mode '): | 202 elif line.startswith('new file mode '): |
203 gp.op = 'ADD' | 203 gp.op = 'ADD' |
204 gp.mode = int(line.rstrip()[-3:], 8) | 204 gp.mode = int(line.rstrip()[-6:], 8) |
205 elif line.startswith('new mode '): | 205 elif line.startswith('new mode '): |
206 gp.mode = int(line.rstrip()[-3:], 8) | 206 gp.mode = int(line.rstrip()[-6:], 8) |
207 elif line.startswith('GIT binary patch'): | 207 elif line.startswith('GIT binary patch'): |
208 dopatch |= GP_BINARY | 208 dopatch |= GP_BINARY |
209 gp.binary = True | 209 gp.binary = True |
210 if gp: | 210 if gp: |
211 gitpatches.append(gp) | 211 gitpatches.append(gp) |
247 returns whether patch was applied with fuzz factor.""" | 247 returns whether patch was applied with fuzz factor.""" |
248 | 248 |
249 fuzz = False | 249 fuzz = False |
250 if cwd: | 250 if cwd: |
251 args.append('-d %s' % util.shellquote(cwd)) | 251 args.append('-d %s' % util.shellquote(cwd)) |
252 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip, | 252 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip, |
253 util.shellquote(patchname))) | 253 util.shellquote(patchname))) |
254 | 254 |
255 for line in fp: | 255 for line in fp: |
256 line = line.rstrip() | 256 line = line.rstrip() |
257 ui.note(line + '\n') | 257 ui.note(line + '\n') |
276 if code: | 276 if code: |
277 raise PatchError(_("patch command failed: %s") % | 277 raise PatchError(_("patch command failed: %s") % |
278 util.explain_exit(code)[0]) | 278 util.explain_exit(code)[0]) |
279 return fuzz | 279 return fuzz |
280 | 280 |
281 def internalpatch(patchname, ui, strip, cwd, files): | 281 def internalpatch(patchobj, ui, strip, cwd, files={}): |
282 """use builtin patch to apply <patchname> to the working directory. | 282 """use builtin patch to apply <patchobj> to the working directory. |
283 returns whether patch was applied with fuzz factor.""" | 283 returns whether patch was applied with fuzz factor.""" |
284 fp = file(patchname, 'rb') | 284 try: |
285 fp = file(patchobj, 'rb') | |
286 except TypeError: | |
287 fp = patchobj | |
285 if cwd: | 288 if cwd: |
286 curdir = os.getcwd() | 289 curdir = os.getcwd() |
287 os.chdir(cwd) | 290 os.chdir(cwd) |
288 try: | 291 try: |
289 ret = applydiff(ui, fp, files, strip=strip) | 292 ret = applydiff(ui, fp, files, strip=strip) |
297 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1 | 300 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1 |
298 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@') | 301 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@') |
299 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)') | 302 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)') |
300 | 303 |
301 class patchfile: | 304 class patchfile: |
302 def __init__(self, ui, fname): | 305 def __init__(self, ui, fname, missing=False): |
303 self.fname = fname | 306 self.fname = fname |
304 self.ui = ui | 307 self.ui = ui |
305 try: | 308 self.lines = [] |
306 fp = file(fname, 'rb') | 309 self.exists = False |
307 self.lines = fp.readlines() | 310 self.missing = missing |
308 self.exists = True | 311 if not missing: |
309 except IOError: | 312 try: |
313 fp = file(fname, 'rb') | |
314 self.lines = fp.readlines() | |
315 self.exists = True | |
316 except IOError: | |
317 pass | |
318 else: | |
319 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname) | |
320 | |
321 if not self.exists: | |
310 dirname = os.path.dirname(fname) | 322 dirname = os.path.dirname(fname) |
311 if dirname and not os.path.isdir(dirname): | 323 if dirname and not os.path.isdir(dirname): |
312 dirs = dirname.split(os.path.sep) | 324 os.makedirs(dirname) |
313 d = "" | 325 |
314 for x in dirs: | |
315 d = os.path.join(d, x) | |
316 if not os.path.isdir(d): | |
317 os.mkdir(d) | |
318 self.lines = [] | |
319 self.exists = False | |
320 | |
321 self.hash = {} | 326 self.hash = {} |
322 self.dirty = 0 | 327 self.dirty = 0 |
323 self.offset = 0 | 328 self.offset = 0 |
324 self.rej = [] | 329 self.rej = [] |
325 self.fileprinted = False | 330 self.fileprinted = False |
344 # from linenum | 349 # from linenum |
345 def sorter(a, b): | 350 def sorter(a, b): |
346 vala = abs(a - linenum) | 351 vala = abs(a - linenum) |
347 valb = abs(b - linenum) | 352 valb = abs(b - linenum) |
348 return cmp(vala, valb) | 353 return cmp(vala, valb) |
349 | 354 |
350 try: | 355 try: |
351 cand = self.hash[l] | 356 cand = self.hash[l] |
352 except: | 357 except: |
353 return [] | 358 return [] |
354 | 359 |
355 if len(cand) > 1: | 360 if len(cand) > 1: |
356 # resort our list of potentials forward then back. | 361 # resort our list of potentials forward then back. |
357 cand.sort(cmp=sorter) | 362 cand.sort(sorter) |
358 return cand | 363 return cand |
359 | 364 |
360 def hashlines(self): | 365 def hashlines(self): |
361 self.hash = {} | 366 self.hash = {} |
362 for x in xrange(len(self.lines)): | 367 for x in xrange(len(self.lines)): |
397 if not dest: | 402 if not dest: |
398 dest = self.fname | 403 dest = self.fname |
399 st = None | 404 st = None |
400 try: | 405 try: |
401 st = os.lstat(dest) | 406 st = os.lstat(dest) |
402 if st.st_nlink > 1: | 407 except OSError, inst: |
403 os.unlink(dest) | 408 if inst.errno != errno.ENOENT: |
404 except: pass | 409 raise |
410 if st and st.st_nlink > 1: | |
411 os.unlink(dest) | |
405 fp = file(dest, 'wb') | 412 fp = file(dest, 'wb') |
406 if st: | 413 if st and st.st_nlink > 1: |
407 os.chmod(dest, st.st_mode) | 414 os.chmod(dest, st.st_mode) |
408 fp.writelines(self.lines) | 415 fp.writelines(self.lines) |
409 fp.close() | 416 fp.close() |
410 | 417 |
411 def close(self): | 418 def close(self): |
419 h.lenb)) | 426 h.lenb)) |
420 | 427 |
421 self.hunks += 1 | 428 self.hunks += 1 |
422 if reverse: | 429 if reverse: |
423 h.reverse() | 430 h.reverse() |
431 | |
432 if self.missing: | |
433 self.rej.append(h) | |
434 return -1 | |
424 | 435 |
425 if self.exists and h.createfile(): | 436 if self.exists and h.createfile(): |
426 self.ui.warn(_("file %s already exists\n") % self.fname) | 437 self.ui.warn(_("file %s already exists\n") % self.fname) |
427 self.rej.append(h) | 438 self.rej.append(h) |
428 return -1 | 439 return -1 |
492 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start)) | 503 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start)) |
493 self.rej.append(h) | 504 self.rej.append(h) |
494 return -1 | 505 return -1 |
495 | 506 |
496 class hunk: | 507 class hunk: |
497 def __init__(self, desc, num, lr, context): | 508 def __init__(self, desc, num, lr, context, gitpatch=None): |
498 self.number = num | 509 self.number = num |
499 self.desc = desc | 510 self.desc = desc |
500 self.hunk = [ desc ] | 511 self.hunk = [ desc ] |
501 self.a = [] | 512 self.a = [] |
502 self.b = [] | 513 self.b = [] |
503 if context: | 514 if context: |
504 self.read_context_hunk(lr) | 515 self.read_context_hunk(lr) |
505 else: | 516 else: |
506 self.read_unified_hunk(lr) | 517 self.read_unified_hunk(lr) |
518 self.gitpatch = gitpatch | |
507 | 519 |
508 def read_unified_hunk(self, lr): | 520 def read_unified_hunk(self, lr): |
509 m = unidesc.match(self.desc) | 521 m = unidesc.match(self.desc) |
510 if not m: | 522 if not m: |
511 raise PatchError(_("bad hunk #%d") % self.number) | 523 raise PatchError(_("bad hunk #%d") % self.number) |
656 | 668 |
657 def complete(self): | 669 def complete(self): |
658 return len(self.a) == self.lena and len(self.b) == self.lenb | 670 return len(self.a) == self.lena and len(self.b) == self.lenb |
659 | 671 |
660 def createfile(self): | 672 def createfile(self): |
661 return self.starta == 0 and self.lena == 0 | 673 create = self.gitpatch is None or self.gitpatch.op == 'ADD' |
674 return self.starta == 0 and self.lena == 0 and create | |
662 | 675 |
663 def rmfile(self): | 676 def rmfile(self): |
664 return self.startb == 0 and self.lenb == 0 | 677 remove = self.gitpatch is None or self.gitpatch.op == 'DELETE' |
678 return self.startb == 0 and self.lenb == 0 and remove | |
665 | 679 |
666 def fuzzit(self, l, fuzz, toponly): | 680 def fuzzit(self, l, fuzz, toponly): |
667 # this removes context lines from the top and bottom of list 'l'. It | 681 # this removes context lines from the top and bottom of list 'l'. It |
668 # checks the hunk to make sure only context lines are removed, and then | 682 # checks the hunk to make sure only context lines are removed, and then |
669 # returns a new shortened list of lines. | 683 # returns a new shortened list of lines. |
700 return l[top:len(l)-bot] | 714 return l[top:len(l)-bot] |
701 return l | 715 return l |
702 | 716 |
703 def old(self, fuzz=0, toponly=False): | 717 def old(self, fuzz=0, toponly=False): |
704 return self.fuzzit(self.a, fuzz, toponly) | 718 return self.fuzzit(self.a, fuzz, toponly) |
705 | 719 |
706 def newctrl(self): | 720 def newctrl(self): |
707 res = [] | 721 res = [] |
708 for x in self.hunk: | 722 for x in self.hunk: |
709 c = x[0] | 723 c = x[0] |
710 if c == ' ' or c == '+': | 724 if c == ' ' or c == '+': |
760 len(text), size) | 774 len(text), size) |
761 self.text = text | 775 self.text = text |
762 | 776 |
763 def parsefilename(str): | 777 def parsefilename(str): |
764 # --- filename \t|space stuff | 778 # --- filename \t|space stuff |
765 s = str[4:] | 779 s = str[4:].rstrip('\r\n') |
766 i = s.find('\t') | 780 i = s.find('\t') |
767 if i < 0: | 781 if i < 0: |
768 i = s.find(' ') | 782 i = s.find(' ') |
769 if i < 0: | 783 if i < 0: |
770 return s | 784 return s |
789 return path[i:].rstrip() | 803 return path[i:].rstrip() |
790 | 804 |
791 nulla = afile_orig == "/dev/null" | 805 nulla = afile_orig == "/dev/null" |
792 nullb = bfile_orig == "/dev/null" | 806 nullb = bfile_orig == "/dev/null" |
793 afile = pathstrip(afile_orig, strip) | 807 afile = pathstrip(afile_orig, strip) |
794 gooda = os.path.exists(afile) and not nulla | 808 gooda = not nulla and os.path.exists(afile) |
795 bfile = pathstrip(bfile_orig, strip) | 809 bfile = pathstrip(bfile_orig, strip) |
796 if afile == bfile: | 810 if afile == bfile: |
797 goodb = gooda | 811 goodb = gooda |
798 else: | 812 else: |
799 goodb = os.path.exists(bfile) and not nullb | 813 goodb = not nullb and os.path.exists(bfile) |
800 createfunc = hunk.createfile | 814 createfunc = hunk.createfile |
801 if reverse: | 815 if reverse: |
802 createfunc = hunk.rmfile | 816 createfunc = hunk.rmfile |
803 if not goodb and not gooda and not createfunc(): | 817 missing = not goodb and not gooda and not createfunc() |
804 raise PatchError(_("unable to find %s or %s for patching") % | 818 fname = None |
805 (afile, bfile)) | 819 if not missing: |
806 if gooda and goodb: | 820 if gooda and goodb: |
807 fname = bfile | 821 fname = (afile in bfile) and afile or bfile |
808 if afile in bfile: | 822 elif gooda: |
809 fname = afile | 823 fname = afile |
810 elif gooda: | 824 |
811 fname = afile | 825 if not fname: |
812 elif not nullb: | 826 if not nullb: |
813 fname = bfile | 827 fname = (afile in bfile) and afile or bfile |
814 if afile in bfile: | 828 elif not nulla: |
815 fname = afile | 829 fname = afile |
816 elif not nulla: | 830 else: |
817 fname = afile | 831 raise PatchError(_("undefined source and destination files")) |
818 return fname | 832 |
833 return fname, missing | |
819 | 834 |
820 class linereader: | 835 class linereader: |
821 # simple class to allow pushing lines back into the input stream | 836 # simple class to allow pushing lines back into the input stream |
822 def __init__(self, fp): | 837 def __init__(self, fp): |
823 self.fp = fp | 838 self.fp = fp |
831 l = self.buf[0] | 846 l = self.buf[0] |
832 del self.buf[0] | 847 del self.buf[0] |
833 return l | 848 return l |
834 return self.fp.readline() | 849 return self.fp.readline() |
835 | 850 |
836 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False, | 851 def iterhunks(ui, fp, sourcefile=None): |
837 rejmerge=None, updatedir=None): | 852 """Read a patch and yield the following events: |
838 """reads a patch from fp and tries to apply it. The dict 'changed' is | 853 - ("file", afile, bfile, firsthunk): select a new target file. |
839 filled in with all of the filenames changed by the patch. Returns 0 | 854 - ("hunk", hunk): a new hunk is ready to be applied, follows a |
840 for a clean patch, -1 if any rejects were found and 1 if there was | 855 "file" event. |
841 any fuzz.""" | 856 - ("git", gitchanges): current diff is in git format, gitchanges |
842 | 857 maps filenames to gitpatch records. Unique event. |
843 def scangitpatch(fp, firstline, cwd=None): | 858 """ |
859 | |
860 def scangitpatch(fp, firstline): | |
844 '''git patches can modify a file, then copy that file to | 861 '''git patches can modify a file, then copy that file to |
845 a new file, but expect the source to be the unmodified form. | 862 a new file, but expect the source to be the unmodified form. |
846 So we scan the patch looking for that case so we can do | 863 So we scan the patch looking for that case so we can do |
847 the copies ahead of time.''' | 864 the copies ahead of time.''' |
848 | 865 |
851 pos = fp.tell() | 868 pos = fp.tell() |
852 except IOError: | 869 except IOError: |
853 fp = cStringIO.StringIO(fp.read()) | 870 fp = cStringIO.StringIO(fp.read()) |
854 | 871 |
855 (dopatch, gitpatches) = readgitpatch(fp, firstline) | 872 (dopatch, gitpatches) = readgitpatch(fp, firstline) |
856 for gp in gitpatches: | |
857 if gp.copymod: | |
858 copyfile(gp.oldpath, gp.path, basedir=cwd) | |
859 | |
860 fp.seek(pos) | 873 fp.seek(pos) |
861 | 874 |
862 return fp, dopatch, gitpatches | 875 return fp, dopatch, gitpatches |
863 | 876 |
877 changed = {} | |
864 current_hunk = None | 878 current_hunk = None |
865 current_file = None | |
866 afile = "" | 879 afile = "" |
867 bfile = "" | 880 bfile = "" |
868 state = None | 881 state = None |
869 hunknum = 0 | 882 hunknum = 0 |
870 rejects = 0 | 883 emitfile = False |
871 | 884 |
872 git = False | 885 git = False |
873 gitre = re.compile('diff --git (a/.*) (b/.*)') | 886 gitre = re.compile('diff --git (a/.*) (b/.*)') |
874 | 887 |
875 # our states | 888 # our states |
876 BFILE = 1 | 889 BFILE = 1 |
877 err = 0 | |
878 context = None | 890 context = None |
879 lr = linereader(fp) | 891 lr = linereader(fp) |
880 dopatch = True | 892 dopatch = True |
881 gitworkdone = False | 893 gitworkdone = False |
882 | 894 |
886 if not x: | 898 if not x: |
887 break | 899 break |
888 if current_hunk: | 900 if current_hunk: |
889 if x.startswith('\ '): | 901 if x.startswith('\ '): |
890 current_hunk.fix_newline() | 902 current_hunk.fix_newline() |
891 ret = current_file.apply(current_hunk, reverse) | 903 yield 'hunk', current_hunk |
892 if ret >= 0: | |
893 changed.setdefault(current_file.fname, (None, None)) | |
894 if ret > 0: | |
895 err = 1 | |
896 current_hunk = None | 904 current_hunk = None |
897 gitworkdone = False | 905 gitworkdone = False |
898 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or | 906 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or |
899 ((context or context == None) and x.startswith('***************')))): | 907 ((context or context == None) and x.startswith('***************')))): |
900 try: | 908 try: |
901 if context == None and x.startswith('***************'): | 909 if context == None and x.startswith('***************'): |
902 context = True | 910 context = True |
903 current_hunk = hunk(x, hunknum + 1, lr, context) | 911 gpatch = changed.get(bfile[2:], (None, None))[1] |
912 current_hunk = hunk(x, hunknum + 1, lr, context, gpatch) | |
904 except PatchError, err: | 913 except PatchError, err: |
905 ui.debug(err) | 914 ui.debug(err) |
906 current_hunk = None | 915 current_hunk = None |
907 continue | 916 continue |
908 hunknum += 1 | 917 hunknum += 1 |
909 if not current_file: | 918 if emitfile: |
910 if sourcefile: | 919 emitfile = False |
911 current_file = patchfile(ui, sourcefile) | 920 yield 'file', (afile, bfile, current_hunk) |
912 else: | |
913 current_file = selectfile(afile, bfile, current_hunk, | |
914 strip, reverse) | |
915 current_file = patchfile(ui, current_file) | |
916 elif state == BFILE and x.startswith('GIT binary patch'): | 921 elif state == BFILE and x.startswith('GIT binary patch'): |
917 current_hunk = binhunk(changed[bfile[2:]][1]) | 922 current_hunk = binhunk(changed[bfile[2:]][1]) |
918 if not current_file: | |
919 if sourcefile: | |
920 current_file = patchfile(ui, sourcefile) | |
921 else: | |
922 current_file = selectfile(afile, bfile, current_hunk, | |
923 strip, reverse) | |
924 current_file = patchfile(ui, current_file) | |
925 hunknum += 1 | 923 hunknum += 1 |
924 if emitfile: | |
925 emitfile = False | |
926 yield 'file', (afile, bfile, current_hunk) | |
926 current_hunk.extract(fp) | 927 current_hunk.extract(fp) |
927 elif x.startswith('diff --git'): | 928 elif x.startswith('diff --git'): |
928 # check for git diff, scanning the whole patch file if needed | 929 # check for git diff, scanning the whole patch file if needed |
929 m = gitre.match(x) | 930 m = gitre.match(x) |
930 if m: | 931 if m: |
931 afile, bfile = m.group(1, 2) | 932 afile, bfile = m.group(1, 2) |
932 if not git: | 933 if not git: |
933 git = True | 934 git = True |
934 fp, dopatch, gitpatches = scangitpatch(fp, x) | 935 fp, dopatch, gitpatches = scangitpatch(fp, x) |
936 yield 'git', gitpatches | |
935 for gp in gitpatches: | 937 for gp in gitpatches: |
936 changed[gp.path] = (gp.op, gp) | 938 changed[gp.path] = (gp.op, gp) |
937 # else error? | 939 # else error? |
938 # copy/rename + modify should modify target, not source | 940 # copy/rename + modify should modify target, not source |
939 if changed.get(bfile[2:], (None, None))[0] in ('COPY', | 941 if changed.get(bfile[2:], (None, None))[0] in ('COPY', |
966 context = True | 968 context = True |
967 afile = parsefilename(x) | 969 afile = parsefilename(x) |
968 bfile = parsefilename(l2) | 970 bfile = parsefilename(l2) |
969 | 971 |
970 if newfile: | 972 if newfile: |
971 if current_file: | 973 emitfile = True |
972 current_file.close() | |
973 if rejmerge: | |
974 rejmerge(current_file) | |
975 rejects += len(current_file.rej) | |
976 state = BFILE | 974 state = BFILE |
977 current_file = None | |
978 hunknum = 0 | 975 hunknum = 0 |
979 if current_hunk: | 976 if current_hunk: |
980 if current_hunk.complete(): | 977 if current_hunk.complete(): |
978 yield 'hunk', current_hunk | |
979 else: | |
980 raise PatchError(_("malformed patch %s %s") % (afile, | |
981 current_hunk.desc)) | |
982 | |
983 if hunknum == 0 and dopatch and not gitworkdone: | |
984 raise NoHunks | |
985 | |
986 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False, | |
987 rejmerge=None, updatedir=None): | |
988 """reads a patch from fp and tries to apply it. The dict 'changed' is | |
989 filled in with all of the filenames changed by the patch. Returns 0 | |
990 for a clean patch, -1 if any rejects were found and 1 if there was | |
991 any fuzz.""" | |
992 | |
993 rejects = 0 | |
994 err = 0 | |
995 current_file = None | |
996 gitpatches = None | |
997 | |
998 def closefile(): | |
999 if not current_file: | |
1000 return 0 | |
1001 current_file.close() | |
1002 if rejmerge: | |
1003 rejmerge(current_file) | |
1004 return len(current_file.rej) | |
1005 | |
1006 for state, values in iterhunks(ui, fp, sourcefile): | |
1007 if state == 'hunk': | |
1008 if not current_file: | |
1009 continue | |
1010 current_hunk = values | |
981 ret = current_file.apply(current_hunk, reverse) | 1011 ret = current_file.apply(current_hunk, reverse) |
982 if ret >= 0: | 1012 if ret >= 0: |
983 changed.setdefault(current_file.fname, (None, None)) | 1013 changed.setdefault(current_file.fname, (None, None)) |
984 if ret > 0: | 1014 if ret > 0: |
985 err = 1 | 1015 err = 1 |
1016 elif state == 'file': | |
1017 rejects += closefile() | |
1018 afile, bfile, first_hunk = values | |
1019 try: | |
1020 if sourcefile: | |
1021 current_file = patchfile(ui, sourcefile) | |
1022 else: | |
1023 current_file, missing = selectfile(afile, bfile, first_hunk, | |
1024 strip, reverse) | |
1025 current_file = patchfile(ui, current_file, missing) | |
1026 except PatchError, err: | |
1027 ui.warn(str(err) + '\n') | |
1028 current_file, current_hunk = None, None | |
1029 rejects += 1 | |
1030 continue | |
1031 elif state == 'git': | |
1032 gitpatches = values | |
1033 for gp in gitpatches: | |
1034 if gp.op in ('COPY', 'RENAME'): | |
1035 copyfile(gp.oldpath, gp.path) | |
1036 changed[gp.path] = (gp.op, gp) | |
986 else: | 1037 else: |
987 fname = current_file and current_file.fname or None | 1038 raise util.Abort(_('unsupported parser state: %s') % state) |
988 raise PatchError(_("malformed patch %s %s") % (fname, | 1039 |
989 current_hunk.desc)) | 1040 rejects += closefile() |
990 if current_file: | 1041 |
991 current_file.close() | 1042 if updatedir and gitpatches: |
992 if rejmerge: | |
993 rejmerge(current_file) | |
994 rejects += len(current_file.rej) | |
995 if updatedir and git: | |
996 updatedir(gitpatches) | 1043 updatedir(gitpatches) |
997 if rejects: | 1044 if rejects: |
998 return -1 | 1045 return -1 |
999 if hunknum == 0 and dopatch and not gitworkdone: | |
1000 raise NoHunks | |
1001 return err | 1046 return err |
1002 | 1047 |
1003 def diffopts(ui, opts={}, untrusted=False): | 1048 def diffopts(ui, opts={}, untrusted=False): |
1004 def get(key, name=None): | 1049 def get(key, name=None): |
1005 return (opts.get(key) or | 1050 return (opts.get(key) or |
1025 if cwd: | 1070 if cwd: |
1026 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()] | 1071 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()] |
1027 for f in patches: | 1072 for f in patches: |
1028 ctype, gp = patches[f] | 1073 ctype, gp = patches[f] |
1029 if ctype == 'RENAME': | 1074 if ctype == 'RENAME': |
1030 copies.append((gp.oldpath, gp.path, gp.copymod)) | 1075 copies.append((gp.oldpath, gp.path)) |
1031 removes[gp.oldpath] = 1 | 1076 removes[gp.oldpath] = 1 |
1032 elif ctype == 'COPY': | 1077 elif ctype == 'COPY': |
1033 copies.append((gp.oldpath, gp.path, gp.copymod)) | 1078 copies.append((gp.oldpath, gp.path)) |
1034 elif ctype == 'DELETE': | 1079 elif ctype == 'DELETE': |
1035 removes[gp.path] = 1 | 1080 removes[gp.path] = 1 |
1036 for src, dst, after in copies: | 1081 for src, dst in copies: |
1037 if not after: | |
1038 copyfile(src, dst, repo.root) | |
1039 repo.copy(src, dst) | 1082 repo.copy(src, dst) |
1040 removes = removes.keys() | 1083 removes = removes.keys() |
1041 if removes: | 1084 if removes: |
1042 removes.sort() | 1085 removes.sort() |
1043 repo.remove(removes, True) | 1086 repo.remove(removes, True) |
1044 for f in patches: | 1087 for f in patches: |
1045 ctype, gp = patches[f] | 1088 ctype, gp = patches[f] |
1046 if gp and gp.mode: | 1089 if gp and gp.mode: |
1047 x = gp.mode & 0100 != 0 | 1090 flags = '' |
1091 if gp.mode & 0100: | |
1092 flags = 'x' | |
1093 elif gp.mode & 020000: | |
1094 flags = 'l' | |
1048 dst = os.path.join(repo.root, gp.path) | 1095 dst = os.path.join(repo.root, gp.path) |
1049 # patch won't create empty files | 1096 # patch won't create empty files |
1050 if ctype == 'ADD' and not os.path.exists(dst): | 1097 if ctype == 'ADD' and not os.path.exists(dst): |
1051 repo.wwrite(gp.path, '', x and 'x' or '') | 1098 repo.wwrite(gp.path, '', flags) |
1052 else: | 1099 else: |
1053 util.set_exec(dst, x) | 1100 util.set_flags(dst, flags) |
1054 cmdutil.addremove(repo, cfiles) | 1101 cmdutil.addremove(repo, cfiles) |
1055 files = patches.keys() | 1102 files = patches.keys() |
1056 files.extend([r for r in removes if r not in files]) | 1103 files.extend([r for r in removes if r not in files]) |
1057 files.sort() | 1104 files.sort() |
1058 | 1105 |
1059 return files | 1106 return files |
1060 | 1107 |
1061 def b85diff(fp, to, tn): | 1108 def b85diff(to, tn): |
1062 '''print base85-encoded binary diff''' | 1109 '''print base85-encoded binary diff''' |
1063 def gitindex(text): | 1110 def gitindex(text): |
1064 if not text: | 1111 if not text: |
1065 return '0' * 40 | 1112 return '0' * 40 |
1066 l = len(text) | 1113 l = len(text) |
1140 return | 1187 return |
1141 | 1188 |
1142 if node2: | 1189 if node2: |
1143 ctx2 = context.changectx(repo, node2) | 1190 ctx2 = context.changectx(repo, node2) |
1144 execf2 = ctx2.manifest().execf | 1191 execf2 = ctx2.manifest().execf |
1192 linkf2 = ctx2.manifest().linkf | |
1145 else: | 1193 else: |
1146 ctx2 = context.workingctx(repo) | 1194 ctx2 = context.workingctx(repo) |
1147 execf2 = util.execfunc(repo.root, None) | 1195 execf2 = util.execfunc(repo.root, None) |
1196 linkf2 = util.linkfunc(repo.root, None) | |
1148 if execf2 is None: | 1197 if execf2 is None: |
1149 execf2 = ctx2.parents()[0].manifest().copy().execf | 1198 mc = ctx2.parents()[0].manifest().copy() |
1199 execf2 = mc.execf | |
1200 linkf2 = mc.linkf | |
1150 | 1201 |
1151 # returns False if there was no rename between ctx1 and ctx2 | 1202 # returns False if there was no rename between ctx1 and ctx2 |
1152 # returns None if the file was created between ctx1 and ctx2 | 1203 # returns None if the file was created between ctx1 and ctx2 |
1153 # returns the (file, node) present in ctx1 that was renamed to f in ctx2 | 1204 # returns the (file, node) present in ctx1 that was renamed to f in ctx2 |
1154 def renamed(f): | 1205 # This will only really work if c1 is the Nth 1st parent of c2. |
1155 startrev = ctx1.rev() | 1206 def renamed(c1, c2, man, f): |
1156 c = ctx2 | 1207 startrev = c1.rev() |
1208 c = c2 | |
1157 crev = c.rev() | 1209 crev = c.rev() |
1158 if crev is None: | 1210 if crev is None: |
1159 crev = repo.changelog.count() | 1211 crev = repo.changelog.count() |
1160 orig = f | 1212 orig = f |
1213 files = (f,) | |
1161 while crev > startrev: | 1214 while crev > startrev: |
1162 if f in c.files(): | 1215 if f in files: |
1163 try: | 1216 try: |
1164 src = getfilectx(f, c).renamed() | 1217 src = getfilectx(f, c).renamed() |
1165 except revlog.LookupError: | 1218 except revlog.LookupError: |
1166 return None | 1219 return None |
1167 if src: | 1220 if src: |
1168 f = src[0] | 1221 f = src[0] |
1169 crev = c.parents()[0].rev() | 1222 crev = c.parents()[0].rev() |
1170 # try to reuse | 1223 # try to reuse |
1171 c = getctx(crev) | 1224 c = getctx(crev) |
1172 if f not in man1: | 1225 files = c.files() |
1226 if f not in man: | |
1173 return None | 1227 return None |
1174 if f == orig: | 1228 if f == orig: |
1175 return False | 1229 return False |
1176 return f | 1230 return f |
1177 | 1231 |
1181 hexfunc = repo.ui.debugflag and hex or short | 1235 hexfunc = repo.ui.debugflag and hex or short |
1182 r = [hexfunc(node) for node in [node1, node2] if node] | 1236 r = [hexfunc(node) for node in [node1, node2] if node] |
1183 | 1237 |
1184 if opts.git: | 1238 if opts.git: |
1185 copied = {} | 1239 copied = {} |
1186 for f in added: | 1240 c1, c2 = ctx1, ctx2 |
1187 src = renamed(f) | 1241 files = added |
1242 man = man1 | |
1243 if node2 and ctx1.rev() >= ctx2.rev(): | |
1244 # renamed() starts at c2 and walks back in history until c1. | |
1245 # Since ctx1.rev() >= ctx2.rev(), invert ctx2 and ctx1 to | |
1246 # detect (inverted) copies. | |
1247 c1, c2 = ctx2, ctx1 | |
1248 files = removed | |
1249 man = ctx2.manifest() | |
1250 for f in files: | |
1251 src = renamed(c1, c2, man, f) | |
1188 if src: | 1252 if src: |
1189 copied[f] = src | 1253 copied[f] = src |
1190 srcs = [x[1] for x in copied.items()] | 1254 if ctx1 == c2: |
1255 # invert the copied dict | |
1256 copied = dict([(v, k) for (k, v) in copied.iteritems()]) | |
1257 # If we've renamed file foo to bar (copied['bar'] = 'foo'), | |
1258 # avoid showing a diff for foo if we're going to show | |
1259 # the rename to bar. | |
1260 srcs = [x[1] for x in copied.iteritems() if x[0] in added] | |
1191 | 1261 |
1192 all = modified + added + removed | 1262 all = modified + added + removed |
1193 all.sort() | 1263 all.sort() |
1194 gone = {} | 1264 gone = {} |
1195 | 1265 |
1200 header = [] | 1270 header = [] |
1201 if f in man1: | 1271 if f in man1: |
1202 to = getfilectx(f, ctx1).data() | 1272 to = getfilectx(f, ctx1).data() |
1203 if f not in removed: | 1273 if f not in removed: |
1204 tn = getfilectx(f, ctx2).data() | 1274 tn = getfilectx(f, ctx2).data() |
1275 a, b = f, f | |
1205 if opts.git: | 1276 if opts.git: |
1206 def gitmode(x): | 1277 def gitmode(x, l): |
1207 return x and '100755' or '100644' | 1278 return l and '120000' or (x and '100755' or '100644') |
1208 def addmodehdr(header, omode, nmode): | 1279 def addmodehdr(header, omode, nmode): |
1209 if omode != nmode: | 1280 if omode != nmode: |
1210 header.append('old mode %s\n' % omode) | 1281 header.append('old mode %s\n' % omode) |
1211 header.append('new mode %s\n' % nmode) | 1282 header.append('new mode %s\n' % nmode) |
1212 | 1283 |
1213 a, b = f, f | |
1214 if f in added: | 1284 if f in added: |
1215 mode = gitmode(execf2(f)) | 1285 mode = gitmode(execf2(f), linkf2(f)) |
1216 if f in copied: | 1286 if f in copied: |
1217 a = copied[f] | 1287 a = copied[f] |
1218 omode = gitmode(man1.execf(a)) | 1288 omode = gitmode(man1.execf(a), man1.linkf(a)) |
1219 addmodehdr(header, omode, mode) | 1289 addmodehdr(header, omode, mode) |
1220 if a in removed and a not in gone: | 1290 if a in removed and a not in gone: |
1221 op = 'rename' | 1291 op = 'rename' |
1222 gone[a] = 1 | 1292 gone[a] = 1 |
1223 else: | 1293 else: |
1231 dodiff = 'binary' | 1301 dodiff = 'binary' |
1232 elif f in removed: | 1302 elif f in removed: |
1233 if f in srcs: | 1303 if f in srcs: |
1234 dodiff = False | 1304 dodiff = False |
1235 else: | 1305 else: |
1236 mode = gitmode(man1.execf(f)) | 1306 mode = gitmode(man1.execf(f), man1.linkf(f)) |
1237 header.append('deleted file mode %s\n' % mode) | 1307 header.append('deleted file mode %s\n' % mode) |
1238 else: | 1308 else: |
1239 omode = gitmode(man1.execf(f)) | 1309 omode = gitmode(man1.execf(f), man1.linkf(f)) |
1240 nmode = gitmode(execf2(f)) | 1310 nmode = gitmode(execf2(f), linkf2(f)) |
1241 addmodehdr(header, omode, nmode) | 1311 addmodehdr(header, omode, nmode) |
1242 if util.binary(to) or util.binary(tn): | 1312 if util.binary(to) or util.binary(tn): |
1243 dodiff = 'binary' | 1313 dodiff = 'binary' |
1244 r = None | 1314 r = None |
1245 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b)) | 1315 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b)) |
1246 if dodiff: | 1316 if dodiff: |
1247 if dodiff == 'binary': | 1317 if dodiff == 'binary': |
1248 text = b85diff(fp, to, tn) | 1318 text = b85diff(to, tn) |
1249 else: | 1319 else: |
1250 text = mdiff.unidiff(to, date1, | 1320 text = mdiff.unidiff(to, date1, |
1251 # ctx2 date may be dynamic | 1321 # ctx2 date may be dynamic |
1252 tn, util.datestr(ctx2.date()), | 1322 tn, util.datestr(ctx2.date()), |
1253 f, r, opts=opts) | 1323 a, b, r, opts=opts) |
1254 if text or len(header) > 1: | 1324 if text or len(header) > 1: |
1255 fp.write(''.join(header)) | 1325 fp.write(''.join(header)) |
1256 fp.write(text) | 1326 fp.write(text) |
1257 | 1327 |
1258 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False, | 1328 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False, |
1301 return | 1371 return |
1302 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt") | 1372 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt") |
1303 try: | 1373 try: |
1304 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name) | 1374 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name) |
1305 try: | 1375 try: |
1306 for line in patchlines: print >> p.tochild, line | 1376 for line in patchlines: |
1377 p.tochild.write(line + "\n") | |
1307 p.tochild.close() | 1378 p.tochild.close() |
1308 if p.wait(): return | 1379 if p.wait(): return |
1309 fp = os.fdopen(fd, 'r') | 1380 fp = os.fdopen(fd, 'r') |
1310 stat = [] | 1381 stat = [] |
1311 for line in fp: stat.append(line.lstrip()) | 1382 for line in fp: stat.append(line.lstrip()) |
1312 last = stat.pop() | 1383 last = stat.pop() |
1313 stat.insert(0, last) | 1384 stat.insert(0, last) |
1314 stat = ''.join(stat) | 1385 stat = ''.join(stat) |
1315 if stat.startswith('0 files'): raise ValueError | |
1316 return stat | 1386 return stat |
1317 except: raise | 1387 except: raise |
1318 finally: | 1388 finally: |
1319 try: os.unlink(name) | 1389 try: os.unlink(name) |
1320 except: pass | 1390 except: pass |