comparison mercurial/patch.py @ 5650:5d3e2f918d65

patch: move diff parsing in iterhunks generator
author Patrick Mezard <pmezard@gmail.com>
date Mon, 17 Dec 2007 23:06:01 +0100
parents a583117b536a
children e11940d84606
comparison
equal deleted inserted replaced
5649:a583117b536a 5650:5d3e2f918d65
836 l = self.buf[0] 836 l = self.buf[0]
837 del self.buf[0] 837 del self.buf[0]
838 return l 838 return l
839 return self.fp.readline() 839 return self.fp.readline()
840 840
841 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False, 841 def iterhunks(ui, fp, sourcefile=None):
842 rejmerge=None, updatedir=None): 842 """Read a patch and yield the following events:
843 """reads a patch from fp and tries to apply it. The dict 'changed' is 843 - ("file", afile, bfile, firsthunk): select a new target file.
844 filled in with all of the filenames changed by the patch. Returns 0 844 - ("hunk", hunk): a new hunk is ready to be applied, follows a
845 for a clean patch, -1 if any rejects were found and 1 if there was 845 "file" event.
846 any fuzz.""" 846 - ("git", gitchanges): current diff is in git format, gitchanges
847 847 maps filenames to gitpatch records. Unique event.
848 def scangitpatch(fp, firstline, cwd=None): 848 """
849
850 def scangitpatch(fp, firstline):
849 '''git patches can modify a file, then copy that file to 851 '''git patches can modify a file, then copy that file to
850 a new file, but expect the source to be the unmodified form. 852 a new file, but expect the source to be the unmodified form.
851 So we scan the patch looking for that case so we can do 853 So we scan the patch looking for that case so we can do
852 the copies ahead of time.''' 854 the copies ahead of time.'''
853 855
856 pos = fp.tell() 858 pos = fp.tell()
857 except IOError: 859 except IOError:
858 fp = cStringIO.StringIO(fp.read()) 860 fp = cStringIO.StringIO(fp.read())
859 861
860 (dopatch, gitpatches) = readgitpatch(fp, firstline) 862 (dopatch, gitpatches) = readgitpatch(fp, firstline)
861 for gp in gitpatches:
862 if gp.op in ('COPY', 'RENAME'):
863 copyfile(gp.oldpath, gp.path, basedir=cwd)
864
865 fp.seek(pos) 863 fp.seek(pos)
866 864
867 return fp, dopatch, gitpatches 865 return fp, dopatch, gitpatches
868 866
867 changed = {}
869 current_hunk = None 868 current_hunk = None
870 current_file = None
871 afile = "" 869 afile = ""
872 bfile = "" 870 bfile = ""
873 state = None 871 state = None
874 hunknum = 0 872 hunknum = 0
875 rejects = 0 873 emitfile = False
876 874
877 git = False 875 git = False
878 gitre = re.compile('diff --git (a/.*) (b/.*)') 876 gitre = re.compile('diff --git (a/.*) (b/.*)')
879 877
880 # our states 878 # our states
881 BFILE = 1 879 BFILE = 1
882 err = 0
883 context = None 880 context = None
884 lr = linereader(fp) 881 lr = linereader(fp)
885 dopatch = True 882 dopatch = True
886 gitworkdone = False 883 gitworkdone = False
887
888 def getpatchfile(afile, bfile, hunk):
889 try:
890 if sourcefile:
891 targetfile = patchfile(ui, sourcefile)
892 else:
893 targetfile = selectfile(afile, bfile, hunk,
894 strip, reverse)
895 targetfile = patchfile(ui, targetfile)
896 return targetfile
897 except PatchError, err:
898 ui.warn(str(err) + '\n')
899 return None
900 884
901 while True: 885 while True:
902 newfile = False 886 newfile = False
903 x = lr.readline() 887 x = lr.readline()
904 if not x: 888 if not x:
905 break 889 break
906 if current_hunk: 890 if current_hunk:
907 if x.startswith('\ '): 891 if x.startswith('\ '):
908 current_hunk.fix_newline() 892 current_hunk.fix_newline()
909 ret = current_file.apply(current_hunk, reverse) 893 yield 'hunk', current_hunk
910 if ret >= 0:
911 changed.setdefault(current_file.fname, (None, None))
912 if ret > 0:
913 err = 1
914 current_hunk = None 894 current_hunk = None
915 gitworkdone = False 895 gitworkdone = False
916 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or 896 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
917 ((context or context == None) and x.startswith('***************')))): 897 ((context or context == None) and x.startswith('***************')))):
918 try: 898 try:
922 except PatchError, err: 902 except PatchError, err:
923 ui.debug(err) 903 ui.debug(err)
924 current_hunk = None 904 current_hunk = None
925 continue 905 continue
926 hunknum += 1 906 hunknum += 1
927 if not current_file: 907 if emitfile:
928 current_file = getpatchfile(afile, bfile, current_hunk) 908 emitfile = False
929 if not current_file: 909 yield 'file', (afile, bfile, current_hunk)
930 current_file, current_hunk = None, None
931 rejects += 1
932 continue
933 elif state == BFILE and x.startswith('GIT binary patch'): 910 elif state == BFILE and x.startswith('GIT binary patch'):
934 current_hunk = binhunk(changed[bfile[2:]][1]) 911 current_hunk = binhunk(changed[bfile[2:]][1])
935 hunknum += 1 912 hunknum += 1
936 if not current_file: 913 if emitfile:
937 current_file = getpatchfile(afile, bfile, current_hunk) 914 emitfile = False
938 if not current_file: 915 yield 'file', (afile, bfile, current_hunk)
939 current_file, current_hunk = None, None
940 rejects += 1
941 continue
942 current_hunk.extract(fp) 916 current_hunk.extract(fp)
943 elif x.startswith('diff --git'): 917 elif x.startswith('diff --git'):
944 # check for git diff, scanning the whole patch file if needed 918 # check for git diff, scanning the whole patch file if needed
945 m = gitre.match(x) 919 m = gitre.match(x)
946 if m: 920 if m:
947 afile, bfile = m.group(1, 2) 921 afile, bfile = m.group(1, 2)
948 if not git: 922 if not git:
949 git = True 923 git = True
950 fp, dopatch, gitpatches = scangitpatch(fp, x) 924 fp, dopatch, gitpatches = scangitpatch(fp, x)
925 yield 'git', gitpatches
951 for gp in gitpatches: 926 for gp in gitpatches:
952 changed[gp.path] = (gp.op, gp) 927 changed[gp.path] = (gp.op, gp)
953 # else error? 928 # else error?
954 # copy/rename + modify should modify target, not source 929 # copy/rename + modify should modify target, not source
955 if changed.get(bfile[2:], (None, None))[0] in ('COPY', 930 if changed.get(bfile[2:], (None, None))[0] in ('COPY',
982 context = True 957 context = True
983 afile = parsefilename(x) 958 afile = parsefilename(x)
984 bfile = parsefilename(l2) 959 bfile = parsefilename(l2)
985 960
986 if newfile: 961 if newfile:
987 if current_file: 962 emitfile = True
988 current_file.close()
989 if rejmerge:
990 rejmerge(current_file)
991 rejects += len(current_file.rej)
992 state = BFILE 963 state = BFILE
993 current_file = None
994 hunknum = 0 964 hunknum = 0
995 if current_hunk: 965 if current_hunk:
996 if current_hunk.complete(): 966 if current_hunk.complete():
967 yield 'hunk', current_hunk
968 else:
969 raise PatchError(_("malformed patch %s %s") % (afile,
970 current_hunk.desc))
971
972 if hunknum == 0 and dopatch and not gitworkdone:
973 raise NoHunks
974
975 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
976 rejmerge=None, updatedir=None):
977 """reads a patch from fp and tries to apply it. The dict 'changed' is
978 filled in with all of the filenames changed by the patch. Returns 0
979 for a clean patch, -1 if any rejects were found and 1 if there was
980 any fuzz."""
981
982 rejects = 0
983 err = 0
984 current_file = None
985 gitpatches = None
986
987 def closefile():
988 if not current_file:
989 return 0
990 current_file.close()
991 if rejmerge:
992 rejmerge(current_file)
993 return len(current_file.rej)
994
995 for state, values in iterhunks(ui, fp, sourcefile):
996 if state == 'hunk':
997 if not current_file:
998 continue
999 current_hunk = values
997 ret = current_file.apply(current_hunk, reverse) 1000 ret = current_file.apply(current_hunk, reverse)
998 if ret >= 0: 1001 if ret >= 0:
999 changed.setdefault(current_file.fname, (None, None)) 1002 changed.setdefault(current_file.fname, (None, None))
1000 if ret > 0: 1003 if ret > 0:
1001 err = 1 1004 err = 1
1005 elif state == 'file':
1006 rejects += closefile()
1007 afile, bfile, first_hunk = values
1008 try:
1009 if sourcefile:
1010 current_file = patchfile(ui, sourcefile)
1011 else:
1012 current_file = selectfile(afile, bfile, first_hunk,
1013 strip, reverse)
1014 current_file = patchfile(ui, current_file)
1015 except PatchError, err:
1016 ui.warn(str(err) + '\n')
1017 current_file, current_hunk = None, None
1018 rejects += 1
1019 continue
1020 elif state == 'git':
1021 gitpatches = values
1022 for gp in gitpatches:
1023 if gp.op in ('COPY', 'RENAME'):
1024 copyfile(gp.oldpath, gp.path)
1025 changed[gp.path] = (gp.op, gp)
1002 else: 1026 else:
1003 fname = current_file and current_file.fname or None 1027 raise util.Abort(_('unsupported parser state: %s') % state)
1004 raise PatchError(_("malformed patch %s %s") % (fname, 1028
1005 current_hunk.desc)) 1029 rejects += closefile()
1006 if current_file: 1030
1007 current_file.close() 1031 if updatedir and gitpatches:
1008 if rejmerge:
1009 rejmerge(current_file)
1010 rejects += len(current_file.rej)
1011
1012 if not rejects and hunknum == 0 and dopatch and not gitworkdone:
1013 raise NoHunks
1014 if updatedir and git:
1015 updatedir(gitpatches) 1032 updatedir(gitpatches)
1016 if rejects: 1033 if rejects:
1017 return -1 1034 return -1
1018 return err 1035 return err
1019 1036