Mercurial > public > mercurial-scm > hg
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(): |