comparison mercurial/patch.py @ 8810:ac92775b3b80

Add patch.eol to ignore EOLs when patching (issue1019) The intent is to fix many issues involving patching when win32ext is enabled. With win32ext, the working directory and repository files EOLs are not the same which means that patches made on a non-win32ext host do not apply cleanly because of EOLs discrepancies. A theorically correct approach would be transform either the patched file or the patch content with the encoding/decoding filters used by win32ext. This solution is tricky to implement and invasive, instead we prefer to address the win32ext case, by offering a way to ignore input EOLs when patching and rewriting them when saving the patched result.
author Patrick Mezard <pmezard@gmail.com>
date Mon, 15 Jun 2009 00:03:26 +0200
parents c5f36402daad
children 6c9dce20ed70
comparison
equal deleted inserted replaced
8809:6fce36336e42 8810:ac92775b3b80
226 if not gitpatches: 226 if not gitpatches:
227 dopatch = GP_PATCH 227 dopatch = GP_PATCH
228 228
229 return (dopatch, gitpatches) 229 return (dopatch, gitpatches)
230 230
231 class linereader:
232 # simple class to allow pushing lines back into the input stream
233 def __init__(self, fp, textmode=False):
234 self.fp = fp
235 self.buf = []
236 self.textmode = textmode
237
238 def push(self, line):
239 if line is not None:
240 self.buf.append(line)
241
242 def readline(self):
243 if self.buf:
244 l = self.buf[0]
245 del self.buf[0]
246 return l
247 l = self.fp.readline()
248 if self.textmode and l.endswith('\r\n'):
249 l = l[:-2] + '\n'
250 return l
251
252 def __iter__(self):
253 while 1:
254 l = self.readline()
255 if not l:
256 break
257 yield l
258
231 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1 259 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
232 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@') 260 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
233 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)') 261 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
234 262
235 class patchfile(object): 263 class patchfile(object):
236 def __init__(self, ui, fname, opener, missing=False): 264 def __init__(self, ui, fname, opener, missing=False, eol=None):
237 self.fname = fname 265 self.fname = fname
266 self.eol = eol
238 self.opener = opener 267 self.opener = opener
239 self.ui = ui 268 self.ui = ui
240 self.lines = [] 269 self.lines = []
241 self.exists = False 270 self.exists = False
242 self.missing = missing 271 self.missing = missing
258 self.hunks = 0 287 self.hunks = 0
259 288
260 def readlines(self, fname): 289 def readlines(self, fname):
261 fp = self.opener(fname, 'r') 290 fp = self.opener(fname, 'r')
262 try: 291 try:
263 return fp.readlines() 292 return list(linereader(fp, self.eol is not None))
264 finally: 293 finally:
265 fp.close() 294 fp.close()
266 295
267 def writelines(self, fname, lines): 296 def writelines(self, fname, lines):
268 fp = self.opener(fname, 'w') 297 fp = self.opener(fname, 'w')
269 try: 298 try:
270 fp.writelines(lines) 299 if self.eol and self.eol != '\n':
300 for l in lines:
301 if l and l[-1] == '\n':
302 l = l[:1] + self.eol
303 fp.write(l)
304 else:
305 fp.writelines(lines)
271 finally: 306 finally:
272 fp.close() 307 fp.close()
273 308
274 def unlink(self, fname): 309 def unlink(self, fname):
275 os.unlink(fname) 310 os.unlink(fname)
780 else: 815 else:
781 raise PatchError(_("undefined source and destination files")) 816 raise PatchError(_("undefined source and destination files"))
782 817
783 return fname, missing 818 return fname, missing
784 819
785 class linereader(object):
786 # simple class to allow pushing lines back into the input stream
787 def __init__(self, fp):
788 self.fp = fp
789 self.buf = []
790
791 def push(self, line):
792 if line is not None:
793 self.buf.append(line)
794
795 def readline(self):
796 if self.buf:
797 return self.buf.pop(0)
798 return self.fp.readline()
799
800 def __iter__(self):
801 while 1:
802 l = self.readline()
803 if not l:
804 break
805 yield l
806
807 def scangitpatch(lr, firstline): 820 def scangitpatch(lr, firstline):
808 """ 821 """
809 Git patches can emit: 822 Git patches can emit:
810 - rename a to b 823 - rename a to b
811 - change b 824 - change b
822 try: 835 try:
823 pos = lr.fp.tell() 836 pos = lr.fp.tell()
824 fp = lr.fp 837 fp = lr.fp
825 except IOError: 838 except IOError:
826 fp = cStringIO.StringIO(lr.fp.read()) 839 fp = cStringIO.StringIO(lr.fp.read())
827 gitlr = linereader(fp) 840 gitlr = linereader(fp, lr.textmode)
828 gitlr.push(firstline) 841 gitlr.push(firstline)
829 (dopatch, gitpatches) = readgitpatch(gitlr) 842 (dopatch, gitpatches) = readgitpatch(gitlr)
830 fp.seek(pos) 843 fp.seek(pos)
831 return dopatch, gitpatches 844 return dopatch, gitpatches
832 845
833 def iterhunks(ui, fp, sourcefile=None): 846 def iterhunks(ui, fp, sourcefile=None, textmode=False):
834 """Read a patch and yield the following events: 847 """Read a patch and yield the following events:
835 - ("file", afile, bfile, firsthunk): select a new target file. 848 - ("file", afile, bfile, firsthunk): select a new target file.
836 - ("hunk", hunk): a new hunk is ready to be applied, follows a 849 - ("hunk", hunk): a new hunk is ready to be applied, follows a
837 "file" event. 850 "file" event.
838 - ("git", gitchanges): current diff is in git format, gitchanges 851 - ("git", gitchanges): current diff is in git format, gitchanges
839 maps filenames to gitpatch records. Unique event. 852 maps filenames to gitpatch records. Unique event.
853
854 If textmode is True, input line-endings are normalized to LF.
840 """ 855 """
841 changed = {} 856 changed = {}
842 current_hunk = None 857 current_hunk = None
843 afile = "" 858 afile = ""
844 bfile = "" 859 bfile = ""
848 git = False 863 git = False
849 864
850 # our states 865 # our states
851 BFILE = 1 866 BFILE = 1
852 context = None 867 context = None
853 lr = linereader(fp) 868 lr = linereader(fp, textmode)
854 dopatch = True 869 dopatch = True
855 # gitworkdone is True if a git operation (copy, rename, ...) was 870 # gitworkdone is True if a git operation (copy, rename, ...) was
856 # performed already for the current file. Useful when the file 871 # performed already for the current file. Useful when the file
857 # section may have no hunk. 872 # section may have no hunk.
858 gitworkdone = False 873 gitworkdone = False
952 current_hunk.desc)) 967 current_hunk.desc))
953 968
954 if hunknum == 0 and dopatch and not gitworkdone: 969 if hunknum == 0 and dopatch and not gitworkdone:
955 raise NoHunks 970 raise NoHunks
956 971
957 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False): 972 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False,
958 """reads a patch from fp and tries to apply it. The dict 'changed' is 973 eol=None):
959 filled in with all of the filenames changed by the patch. Returns 0 974 """
960 for a clean patch, -1 if any rejects were found and 1 if there was 975 Reads a patch from fp and tries to apply it.
961 any fuzz.""" 976
962 977 The dict 'changed' is filled in with all of the filenames changed
978 by the patch. Returns 0 for a clean patch, -1 if any rejects were
979 found and 1 if there was any fuzz.
980
981 If 'eol' is None, the patch content and patched file are read in
982 binary mode. Otherwise, line endings are ignored when patching then
983 normalized to 'eol' (usually '\n' or \r\n').
984 """
963 rejects = 0 985 rejects = 0
964 err = 0 986 err = 0
965 current_file = None 987 current_file = None
966 gitpatches = None 988 gitpatches = None
967 opener = util.opener(os.getcwd()) 989 opener = util.opener(os.getcwd())
990 textmode = eol is not None
968 991
969 def closefile(): 992 def closefile():
970 if not current_file: 993 if not current_file:
971 return 0 994 return 0
972 current_file.close() 995 current_file.close()
973 return len(current_file.rej) 996 return len(current_file.rej)
974 997
975 for state, values in iterhunks(ui, fp, sourcefile): 998 for state, values in iterhunks(ui, fp, sourcefile, textmode):
976 if state == 'hunk': 999 if state == 'hunk':
977 if not current_file: 1000 if not current_file:
978 continue 1001 continue
979 current_hunk = values 1002 current_hunk = values
980 ret = current_file.apply(current_hunk, reverse) 1003 ret = current_file.apply(current_hunk, reverse)
985 elif state == 'file': 1008 elif state == 'file':
986 rejects += closefile() 1009 rejects += closefile()
987 afile, bfile, first_hunk = values 1010 afile, bfile, first_hunk = values
988 try: 1011 try:
989 if sourcefile: 1012 if sourcefile:
990 current_file = patchfile(ui, sourcefile, opener) 1013 current_file = patchfile(ui, sourcefile, opener, eol=eol)
991 else: 1014 else:
992 current_file, missing = selectfile(afile, bfile, first_hunk, 1015 current_file, missing = selectfile(afile, bfile, first_hunk,
993 strip, reverse) 1016 strip, reverse)
994 current_file = patchfile(ui, current_file, opener, missing) 1017 current_file = patchfile(ui, current_file, opener, missing, eol)
995 except PatchError, err: 1018 except PatchError, err:
996 ui.warn(str(err) + '\n') 1019 ui.warn(str(err) + '\n')
997 current_file, current_hunk = None, None 1020 current_file, current_hunk = None, None
998 rejects += 1 1021 rejects += 1
999 continue 1022 continue
1102 if code: 1125 if code:
1103 raise PatchError(_("patch command failed: %s") % 1126 raise PatchError(_("patch command failed: %s") %
1104 util.explain_exit(code)[0]) 1127 util.explain_exit(code)[0])
1105 return fuzz 1128 return fuzz
1106 1129
1107 def internalpatch(patchobj, ui, strip, cwd, files={}): 1130 def internalpatch(patchobj, ui, strip, cwd, files={}, eolmode='strict'):
1108 """use builtin patch to apply <patchobj> to the working directory. 1131 """use builtin patch to apply <patchobj> to the working directory.
1109 returns whether patch was applied with fuzz factor.""" 1132 returns whether patch was applied with fuzz factor."""
1133
1134 if eolmode is None:
1135 eolmode = ui.config('patch', 'eol', 'strict')
1136 try:
1137 eol = {'strict': None, 'crlf': '\r\n', 'lf': '\n'}[eolmode.lower()]
1138 except KeyError:
1139 raise util.Abort(_('Unsupported line endings type: %s') % eolmode)
1140
1110 try: 1141 try:
1111 fp = file(patchobj, 'rb') 1142 fp = file(patchobj, 'rb')
1112 except TypeError: 1143 except TypeError:
1113 fp = patchobj 1144 fp = patchobj
1114 if cwd: 1145 if cwd:
1115 curdir = os.getcwd() 1146 curdir = os.getcwd()
1116 os.chdir(cwd) 1147 os.chdir(cwd)
1117 try: 1148 try:
1118 ret = applydiff(ui, fp, files, strip=strip) 1149 ret = applydiff(ui, fp, files, strip=strip, eol=eol)
1119 finally: 1150 finally:
1120 if cwd: 1151 if cwd:
1121 os.chdir(curdir) 1152 os.chdir(curdir)
1122 if ret < 0: 1153 if ret < 0:
1123 raise PatchError 1154 raise PatchError
1124 return ret > 0 1155 return ret > 0
1125 1156
1126 def patch(patchname, ui, strip=1, cwd=None, files={}): 1157 def patch(patchname, ui, strip=1, cwd=None, files={}, eolmode='strict'):
1127 """apply <patchname> to the working directory. 1158 """Apply <patchname> to the working directory.
1128 returns whether patch was applied with fuzz factor.""" 1159
1160 'eolmode' specifies how end of lines should be handled. It can be:
1161 - 'strict': inputs are read in binary mode, EOLs are preserved
1162 - 'crlf': EOLs are ignored when patching and reset to CRLF
1163 - 'lf': EOLs are ignored when patching and reset to LF
1164 - None: get it from user settings, default to 'strict'
1165 'eolmode' is ignored when using an external patcher program.
1166
1167 Returns whether patch was applied with fuzz factor.
1168 """
1129 patcher = ui.config('ui', 'patch') 1169 patcher = ui.config('ui', 'patch')
1130 args = [] 1170 args = []
1131 try: 1171 try:
1132 if patcher: 1172 if patcher:
1133 return externalpatch(patcher, args, patchname, ui, strip, cwd, 1173 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1134 files) 1174 files)
1135 else: 1175 else:
1136 try: 1176 try:
1137 return internalpatch(patchname, ui, strip, cwd, files) 1177 return internalpatch(patchname, ui, strip, cwd, files, eolmode)
1138 except NoHunks: 1178 except NoHunks:
1139 patcher = util.find_exe('gpatch') or util.find_exe('patch') or 'patch' 1179 patcher = util.find_exe('gpatch') or util.find_exe('patch') or 'patch'
1140 ui.debug(_('no valid hunks found; trying with %r instead\n') % 1180 ui.debug(_('no valid hunks found; trying with %r instead\n') %
1141 patcher) 1181 patcher)
1142 if util.needbinarypatch(): 1182 if util.needbinarypatch():