comparison mercurial/util.py @ 2859:b3d1145ed06c

Teach import to understand git diff extensions. Vanilla patch chokes on git patches that include files that are copied or renamed, then modified. So this code detects that case and rewrites the patch if necessary.
author Brendan Cully <brendan@kublai.com>
date Fri, 11 Aug 2006 15:50:07 -0700
parents 1e8b8107a2c9
children 0f08f2c042ec
comparison
equal deleted inserted replaced
2842:7706fa503677 2859:b3d1145ed06c
91 p_name = os.path.join(p, name) 91 p_name = os.path.join(p, name)
92 if os.path.exists(p_name): 92 if os.path.exists(p_name):
93 return p_name 93 return p_name
94 return default 94 return default
95 95
96 def readgitpatch(patchname):
97 """extract git-style metadata about patches from <patchname>"""
98 class gitpatch:
99 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
100 def __init__(self, path):
101 self.path = path
102 self.oldpath = None
103 self.mode = None
104 self.op = 'MODIFY'
105 self.copymod = False
106 self.lineno = 0
107
108 # Filter patch for git information
109 gitre = re.compile('diff --git a/(.*) b/(.*)')
110 pf = file(patchname)
111 gp = None
112 gitpatches = []
113 # Can have a git patch with only metadata, causing patch to complain
114 dopatch = False
115
116 lineno = 0
117 for line in pf:
118 lineno += 1
119 if line.startswith('diff --git'):
120 m = gitre.match(line)
121 if m:
122 if gp:
123 gitpatches.append(gp)
124 src, dst = m.group(1,2)
125 gp = gitpatch(dst)
126 gp.lineno = lineno
127 elif gp:
128 if line.startswith('--- '):
129 if gp.op in ('COPY', 'RENAME'):
130 gp.copymod = True
131 dopatch = 'filter'
132 gitpatches.append(gp)
133 gp = None
134 if not dopatch:
135 dopatch = True
136 continue
137 if line.startswith('rename from '):
138 gp.op = 'RENAME'
139 gp.oldpath = line[12:].rstrip()
140 elif line.startswith('rename to '):
141 gp.path = line[10:].rstrip()
142 elif line.startswith('copy from '):
143 gp.op = 'COPY'
144 gp.oldpath = line[10:].rstrip()
145 elif line.startswith('copy to '):
146 gp.path = line[8:].rstrip()
147 elif line.startswith('deleted file'):
148 gp.op = 'DELETE'
149 elif line.startswith('new file mode '):
150 gp.op = 'ADD'
151 gp.mode = int(line.rstrip()[-3:], 8)
152 elif line.startswith('new mode '):
153 gp.mode = int(line.rstrip()[-3:], 8)
154 if gp:
155 gitpatches.append(gp)
156
157 if not gitpatches:
158 dopatch = True
159
160 return (dopatch, gitpatches)
161
162 def dogitpatch(patchname, gitpatches):
163 """Preprocess git patch so that vanilla patch can handle it"""
164 pf = file(patchname)
165 pfline = 1
166
167 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
168 tmpfp = os.fdopen(fd, 'w')
169
170 try:
171 for i in range(len(gitpatches)):
172 p = gitpatches[i]
173 if not p.copymod:
174 continue
175
176 if os.path.exists(p.path):
177 raise Abort(_("cannot create %s: destination already exists") %
178 p.path)
179
180 (src, dst) = [os.path.join(os.getcwd(), n)
181 for n in (p.oldpath, p.path)]
182
183 print "copying %s to %s" % (src, dst)
184 targetdir = os.path.dirname(dst)
185 if not os.path.isdir(targetdir):
186 os.makedirs(targetdir)
187 try:
188 shutil.copyfile(src, dst)
189 shutil.copymode(src, dst)
190 except shutil.Error, inst:
191 raise Abort(str(inst))
192
193 # rewrite patch hunk
194 while pfline < p.lineno:
195 tmpfp.write(pf.readline())
196 pfline += 1
197 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
198 line = pf.readline()
199 pfline += 1
200 while not line.startswith('--- a/'):
201 tmpfp.write(line)
202 line = pf.readline()
203 pfline += 1
204 tmpfp.write('--- a/%s\n' % p.path)
205
206 line = pf.readline()
207 while line:
208 tmpfp.write(line)
209 line = pf.readline()
210 except:
211 tmpfp.close()
212 os.unlink(patchname)
213 raise
214
215 tmpfp.close()
216 return patchname
217
96 def patch(strip, patchname, ui, cwd=None): 218 def patch(strip, patchname, ui, cwd=None):
97 """apply the patch <patchname> to the working directory. 219 """apply the patch <patchname> to the working directory.
98 a list of patched files is returned""" 220 a list of patched files is returned"""
99 patcher = find_in_path('gpatch', os.environ.get('PATH', ''), 'patch') 221
100 args = [] 222 (dopatch, gitpatches) = readgitpatch(patchname)
101 if cwd: 223
102 args.append('-d %s' % shellquote(cwd))
103 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
104 shellquote(patchname)))
105 files = {} 224 files = {}
106 for line in fp: 225 if dopatch:
107 line = line.rstrip() 226 if dopatch == 'filter':
108 ui.status("%s\n" % line) 227 patchname = dogitpatch(patchname, gitpatches)
109 if line.startswith('patching file '): 228 patcher = find_in_path('gpatch', os.environ.get('PATH', ''), 'patch')
110 pf = parse_patch_output(line) 229 args = []
111 files.setdefault(pf, 1) 230 if cwd:
112 code = fp.close() 231 args.append('-d %s' % shellquote(cwd))
113 if code: 232 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
114 raise Abort(_("patch command failed: %s") % explain_exit(code)[0]) 233 shellquote(patchname)))
115 return files.keys() 234
235 if dopatch == 'filter':
236 False and os.unlink(patchname)
237
238 for line in fp:
239 line = line.rstrip()
240 ui.status("%s\n" % line)
241 if line.startswith('patching file '):
242 pf = parse_patch_output(line)
243 files.setdefault(pf, (None, None))
244 code = fp.close()
245 if code:
246 raise Abort(_("patch command failed: %s") % explain_exit(code)[0])
247
248 for gp in gitpatches:
249 files[gp.path] = (gp.op, gp)
250
251 return files
116 252
117 def binary(s): 253 def binary(s):
118 """return true if a string is binary data using diff's heuristic""" 254 """return true if a string is binary data using diff's heuristic"""
119 if s and '\0' in s[:4096]: 255 if s and '\0' in s[:4096]:
120 return True 256 return True