comparison mercurial/patch.py @ 10189:e451e599fbcf

patch: support diff data loss detection and upgrade In worst case, generating diff in upgrade mode can be two times more expensive than generating it in git mode directly: we may have to regenerate the whole diff again whenever a git feature is detected. Also, the first diff attempt is completely buffered instead of being streamed. That said, even without having profiled it yet, I am convinced we can fast-path the upgrade mode if necessary were it to be used in regular diff commands, and not only in mq where avoiding data loss is worth the price.
author Patrick Mezard <pmezard@gmail.com>
date Fri, 01 Jan 2010 20:54:05 +0100
parents c7355a0e1f39
children 3ca8f2ae5fee
comparison
equal deleted inserted replaced
10188:fd6e9c7cd98c 10189:e451e599fbcf
1244 for l in chunk(zlib.compress(tn)): 1244 for l in chunk(zlib.compress(tn)):
1245 ret.append(fmtline(l)) 1245 ret.append(fmtline(l))
1246 ret.append('\n') 1246 ret.append('\n')
1247 return ''.join(ret) 1247 return ''.join(ret)
1248 1248
1249 def _addmodehdr(header, omode, nmode): 1249 class GitDiffRequired(Exception):
1250 if omode != nmode: 1250 pass
1251 header.append('old mode %s\n' % omode) 1251
1252 header.append('new mode %s\n' % nmode) 1252 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1253 1253 losedatafn=None):
1254 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None):
1255 '''yields diff of changes to files between two nodes, or node and 1254 '''yields diff of changes to files between two nodes, or node and
1256 working directory. 1255 working directory.
1257 1256
1258 if node1 is None, use first dirstate parent instead. 1257 if node1 is None, use first dirstate parent instead.
1259 if node2 is None, compare node1 with working directory.''' 1258 if node2 is None, compare node1 with working directory.
1259
1260 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1261 every time some change cannot be represented with the current
1262 patch format. Return False to upgrade to git patch format, True to
1263 accept the loss or raise an exception to abort the diff. It is
1264 called with the name of current file being diffed as 'fn'. If set
1265 to None, patches will always be upgraded to git format when
1266 necessary.
1267 '''
1260 1268
1261 if opts is None: 1269 if opts is None:
1262 opts = mdiff.defaultopts 1270 opts = mdiff.defaultopts
1263 1271
1264 if not node1 and not node2: 1272 if not node1 and not node2:
1286 if not changes: 1294 if not changes:
1287 changes = repo.status(ctx1, ctx2, match=match) 1295 changes = repo.status(ctx1, ctx2, match=match)
1288 modified, added, removed = changes[:3] 1296 modified, added, removed = changes[:3]
1289 1297
1290 if not modified and not added and not removed: 1298 if not modified and not added and not removed:
1291 return 1299 return []
1292
1293 date1 = util.datestr(ctx1.date())
1294 man1 = ctx1.manifest()
1295 1300
1296 revs = None 1301 revs = None
1297 if not repo.ui.quiet and not opts.git: 1302 if not repo.ui.quiet:
1298 hexfunc = repo.ui.debugflag and hex or short 1303 hexfunc = repo.ui.debugflag and hex or short
1299 revs = [hexfunc(node) for node in [node1, node2] if node] 1304 revs = [hexfunc(node) for node in [node1, node2] if node]
1300 1305
1301 if opts.git: 1306 copy = {}
1302 copy, diverge = copies.copies(repo, ctx1, ctx2, repo[nullid]) 1307 if opts.git or opts.upgrade:
1308 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1303 copy = copy.copy() 1309 copy = copy.copy()
1304 for k, v in copy.items(): 1310 for k, v in copy.items():
1305 copy[v] = k 1311 copy[v] = k
1306 1312
1313 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1314 modified, added, removed, copy, getfilectx, opts, losedata)
1315 if opts.upgrade and not opts.git:
1316 try:
1317 def losedata(fn):
1318 if not losedatafn or not losedatafn(fn=fn):
1319 raise GitDiffRequired()
1320 # Buffer the whole output until we are sure it can be generated
1321 return list(difffn(opts.copy(git=False), losedata))
1322 except GitDiffRequired:
1323 return difffn(opts.copy(git=True), None)
1324 else:
1325 return difffn(opts, None)
1326
1327 def _addmodehdr(header, omode, nmode):
1328 if omode != nmode:
1329 header.append('old mode %s\n' % omode)
1330 header.append('new mode %s\n' % nmode)
1331
1332 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1333 copy, getfilectx, opts, losedatafn):
1334
1335 date1 = util.datestr(ctx1.date())
1336 man1 = ctx1.manifest()
1337
1307 gone = set() 1338 gone = set()
1308 gitmode = {'l': '120000', 'x': '100755', '': '100644'} 1339 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1340
1341 if opts.git:
1342 revs = None
1309 1343
1310 for f in sorted(modified + added + removed): 1344 for f in sorted(modified + added + removed):
1311 to = None 1345 to = None
1312 tn = None 1346 tn = None
1313 dodiff = True 1347 dodiff = True
1315 if f in man1: 1349 if f in man1:
1316 to = getfilectx(f, ctx1).data() 1350 to = getfilectx(f, ctx1).data()
1317 if f not in removed: 1351 if f not in removed:
1318 tn = getfilectx(f, ctx2).data() 1352 tn = getfilectx(f, ctx2).data()
1319 a, b = f, f 1353 a, b = f, f
1320 if opts.git: 1354 if opts.git or losedatafn:
1321 if f in added: 1355 if f in added:
1322 mode = gitmode[ctx2.flags(f)] 1356 mode = gitmode[ctx2.flags(f)]
1323 if f in copy: 1357 if f in copy:
1324 a = copy[f] 1358 if opts.git:
1325 omode = gitmode[man1.flags(a)] 1359 a = copy[f]
1326 _addmodehdr(header, omode, mode) 1360 omode = gitmode[man1.flags(a)]
1327 if a in removed and a not in gone: 1361 _addmodehdr(header, omode, mode)
1328 op = 'rename' 1362 if a in removed and a not in gone:
1329 gone.add(a) 1363 op = 'rename'
1364 gone.add(a)
1365 else:
1366 op = 'copy'
1367 header.append('%s from %s\n' % (op, a))
1368 header.append('%s to %s\n' % (op, f))
1369 to = getfilectx(a, ctx1).data()
1330 else: 1370 else:
1331 op = 'copy' 1371 losedatafn(f)
1332 header.append('%s from %s\n' % (op, a))
1333 header.append('%s to %s\n' % (op, f))
1334 to = getfilectx(a, ctx1).data()
1335 else: 1372 else:
1336 header.append('new file mode %s\n' % mode) 1373 if opts.git:
1374 header.append('new file mode %s\n' % mode)
1375 elif ctx2.flags(f):
1376 losedatafn(f)
1337 if util.binary(tn): 1377 if util.binary(tn):
1338 dodiff = 'binary' 1378 if opts.git:
1379 dodiff = 'binary'
1380 else:
1381 losedatafn(f)
1382 if not opts.git and not tn:
1383 # regular diffs cannot represent new empty file
1384 losedatafn(f)
1339 elif f in removed: 1385 elif f in removed:
1340 # have we already reported a copy above? 1386 if opts.git:
1341 if f in copy and copy[f] in added and copy[copy[f]] == f: 1387 # have we already reported a copy above?
1342 dodiff = False 1388 if f in copy and copy[f] in added and copy[copy[f]] == f:
1343 else: 1389 dodiff = False
1344 header.append('deleted file mode %s\n' % 1390 else:
1345 gitmode[man1.flags(f)]) 1391 header.append('deleted file mode %s\n' %
1392 gitmode[man1.flags(f)])
1393 elif not to:
1394 # regular diffs cannot represent empty file deletion
1395 losedatafn(f)
1346 else: 1396 else:
1347 omode = gitmode[man1.flags(f)] 1397 oflag = man1.flags(f)
1348 nmode = gitmode[ctx2.flags(f)] 1398 nflag = ctx2.flags(f)
1349 _addmodehdr(header, omode, nmode) 1399 binary = util.binary(to) or util.binary(tn)
1350 if util.binary(to) or util.binary(tn): 1400 if opts.git:
1351 dodiff = 'binary' 1401 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1352 header.insert(0, mdiff.diffline(revs, a, b, opts)) 1402 if binary:
1403 dodiff = 'binary'
1404 elif binary or nflag != oflag:
1405 losedatafn(f)
1406 if opts.git:
1407 header.insert(0, mdiff.diffline(revs, a, b, opts))
1408
1353 if dodiff: 1409 if dodiff:
1354 if dodiff == 'binary': 1410 if dodiff == 'binary':
1355 text = b85diff(to, tn) 1411 text = b85diff(to, tn)
1356 else: 1412 else:
1357 text = mdiff.unidiff(to, date1, 1413 text = mdiff.unidiff(to, date1,