comparison mercurial/patch.py @ 16506:fc4e0fecf403 stable

patch: fix patch hunk/metdata synchronization (issue3384) Git patches are parsed in two phases: 1) extract metadata, 2) parse actual deltas and merge them with the previous metadata. We do this to avoid dependency issues like "modify a; copy a to b", where "b" must be copied from the unmodified "a". Issue3384 is caused by flaky code I wrote to synchronize the patch metadata with the emitted hunk: if (gitpatches and (gitpatches[-1][0] == afile or gitpatches[-1][1] == bfile)): gp = gitpatches.pop()[2] With a patch like: diff --git a/a b/c copy from a copy to c --- a/a +++ b/c @@ -1,1 +1,2 @@ a +a @@ -2,1 +2,2 @@ a +a diff --git a/a b/a --- a/a +++ b/a @@ -1,1 +1,2 @@ a +b the first hunk of the first block is matched with the metadata for the block "diff --git a/a b/c", then the second hunk of the first block is matched with the metadata of the second block "diff --git a/a b/a", because of the "or" in the code paste above. Turning the "or" into an "and" is not enough as we have to deal with /dev/null cases for each file. We I remove this broken piece of code: # copy/rename + modify should modify target, not source if gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD') or gp.mode: afile = bfile because "afile = bfile" set "afile" to stuff like "b/file" instead of "a/file", and because this only happens for git patches, which afile/bfile are ignored anyway by applydiff(). v2: - Avoid a traceback on git metadata desynchronization
author Patrick Mezard <patrick@mezard.eu>
date Sat, 21 Apr 2012 21:40:25 +0200
parents 1f75c1decdeb
children a8065323c003
comparison
equal deleted inserted replaced
16505:db85c24dcdea 16506:fc4e0fecf403
287 other.oldpath = self.oldpath 287 other.oldpath = self.oldpath
288 other.mode = self.mode 288 other.mode = self.mode
289 other.op = self.op 289 other.op = self.op
290 other.binary = self.binary 290 other.binary = self.binary
291 return other 291 return other
292
293 def _ispatchinga(self, afile):
294 if afile == '/dev/null':
295 return self.op == 'ADD'
296 return afile == 'a/' + (self.oldpath or self.path)
297
298 def _ispatchingb(self, bfile):
299 if bfile == '/dev/null':
300 return self.op == 'DELETE'
301 return bfile == 'b/' + self.path
302
303 def ispatching(self, afile, bfile):
304 return self._ispatchinga(afile) and self._ispatchingb(bfile)
292 305
293 def __repr__(self): 306 def __repr__(self):
294 return "<patchmeta %s %r>" % (self.op, self.path) 307 return "<patchmeta %s %r>" % (self.op, self.path)
295 308
296 def readgitpatch(lr): 309 def readgitpatch(lr):
1178 (not context and x[0] == '@') 1191 (not context and x[0] == '@')
1179 or (context is not False and x.startswith('***************')) 1192 or (context is not False and x.startswith('***************'))
1180 or x.startswith('GIT binary patch')): 1193 or x.startswith('GIT binary patch')):
1181 gp = None 1194 gp = None
1182 if (gitpatches and 1195 if (gitpatches and
1183 (gitpatches[-1][0] == afile or gitpatches[-1][1] == bfile)): 1196 gitpatches[-1].ispatching(afile, bfile)):
1184 gp = gitpatches.pop()[2] 1197 gp = gitpatches.pop()
1185 if x.startswith('GIT binary patch'): 1198 if x.startswith('GIT binary patch'):
1186 h = binhunk(lr) 1199 h = binhunk(lr)
1187 else: 1200 else:
1188 if context is None and x.startswith('***************'): 1201 if context is None and x.startswith('***************'):
1189 context = True 1202 context = True
1195 yield 'hunk', h 1208 yield 'hunk', h
1196 elif x.startswith('diff --git'): 1209 elif x.startswith('diff --git'):
1197 m = gitre.match(x) 1210 m = gitre.match(x)
1198 if not m: 1211 if not m:
1199 continue 1212 continue
1200 if not gitpatches: 1213 if gitpatches is None:
1201 # scan whole input for git metadata 1214 # scan whole input for git metadata
1202 gitpatches = [('a/' + gp.path, 'b/' + gp.path, gp) for gp 1215 gitpatches = scangitpatch(lr, x)
1203 in scangitpatch(lr, x)] 1216 yield 'git', [g.copy() for g in gitpatches
1204 yield 'git', [g[2].copy() for g in gitpatches 1217 if g.op in ('COPY', 'RENAME')]
1205 if g[2].op in ('COPY', 'RENAME')]
1206 gitpatches.reverse() 1218 gitpatches.reverse()
1207 afile = 'a/' + m.group(1) 1219 afile = 'a/' + m.group(1)
1208 bfile = 'b/' + m.group(2) 1220 bfile = 'b/' + m.group(2)
1209 while afile != gitpatches[-1][0] and bfile != gitpatches[-1][1]: 1221 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1210 gp = gitpatches.pop()[2] 1222 gp = gitpatches.pop()
1211 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy()) 1223 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1212 gp = gitpatches[-1][2] 1224 if not gitpatches:
1213 # copy/rename + modify should modify target, not source 1225 raise PatchError(_('failed to synchronize metadata for "%s"')
1214 if gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD') or gp.mode: 1226 % afile[2:])
1215 afile = bfile 1227 gp = gitpatches[-1]
1216 newfile = True 1228 newfile = True
1217 elif x.startswith('---'): 1229 elif x.startswith('---'):
1218 # check for a unified diff 1230 # check for a unified diff
1219 l2 = lr.readline() 1231 l2 = lr.readline()
1220 if not l2.startswith('+++'): 1232 if not l2.startswith('+++'):
1245 emitfile = True 1257 emitfile = True
1246 state = BFILE 1258 state = BFILE
1247 hunknum = 0 1259 hunknum = 0
1248 1260
1249 while gitpatches: 1261 while gitpatches:
1250 gp = gitpatches.pop()[2] 1262 gp = gitpatches.pop()
1251 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy()) 1263 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1252 1264
1253 def applydiff(ui, fp, backend, store, strip=1, eolmode='strict'): 1265 def applydiff(ui, fp, backend, store, strip=1, eolmode='strict'):
1254 """Reads a patch from fp and tries to apply it. 1266 """Reads a patch from fp and tries to apply it.
1255 1267