comparison mercurial/merge.py @ 42456:87a34c767384

merge: fix race that could cause wrong size in dirstate The problem is that hg merge/update/etc work the following way: 1. figure out what files to update 2. apply the update to disk 3. apply the update to in-memory dirstate 4. write dirstate where step3 looks at the filesystem and assumes it sees the result of step2. If a file is changed between step2 and step3, step3 will record incorrect information in the dirstate. I avoid this by passing the size step3 needs directly from step2, for the common path (not implemented for change/delete conflicts for instance). I didn't fix the same race for the exec bit for now, because it's less likely to be problematic and I had trouble due to the fact that the dirstate stores the permissions differently from the manifest (st_mode vs '' 'l' 'x'), in combination with tests that pretend that symlinks are not supported. However, I moved the lstat from step3 to step2, which should tighten the race window markedly, both for the exec bit and for the mtime. Differential Revision: https://phab.mercurial-scm.org/D6475
author Valentin Gatien-Baron <valentin.gatienbaron@gmail.com>
date Mon, 27 May 2019 16:55:46 -0400
parents 127937874395
children d29db0a0c4eb
comparison
equal deleted inserted replaced
42455:5ca136bbd3f6 42456:87a34c767384
8 from __future__ import absolute_import 8 from __future__ import absolute_import
9 9
10 import errno 10 import errno
11 import hashlib 11 import hashlib
12 import shutil 12 import shutil
13 import stat
13 import struct 14 import struct
14 15
15 from .i18n import _ 16 from .i18n import _
16 from .node import ( 17 from .node import (
17 addednodeid, 18 addednodeid,
681 return actions 682 return actions
682 683
683 def recordactions(self): 684 def recordactions(self):
684 """record remove/add/get actions in the dirstate""" 685 """record remove/add/get actions in the dirstate"""
685 branchmerge = self._repo.dirstate.p2() != nullid 686 branchmerge = self._repo.dirstate.p2() != nullid
686 recordupdates(self._repo, self.actions(), branchmerge) 687 recordupdates(self._repo, self.actions(), branchmerge, None)
687 688
688 def queueremove(self, f): 689 def queueremove(self, f):
689 """queues a file to be removed from the dirstate 690 """queues a file to be removed from the dirstate
690 691
691 Meant for use by custom merge drivers.""" 692 Meant for use by custom merge drivers."""
1462 # cwd was removed in the course of removing files; print a helpful 1463 # cwd was removed in the course of removing files; print a helpful
1463 # warning. 1464 # warning.
1464 repo.ui.warn(_("current directory was removed\n" 1465 repo.ui.warn(_("current directory was removed\n"
1465 "(consider changing to repo root: %s)\n") % repo.root) 1466 "(consider changing to repo root: %s)\n") % repo.root)
1466 1467
1467 def batchget(repo, mctx, wctx, actions): 1468 def batchget(repo, mctx, wctx, wantfiledata, actions):
1468 """apply gets to the working directory 1469 """apply gets to the working directory
1469 1470
1470 mctx is the context to get from 1471 mctx is the context to get from
1471 1472
1472 yields tuples for progress updates 1473 Yields arbitrarily many (False, tuple) for progress updates, followed by
1474 exactly one (True, filedata). When wantfiledata is false, filedata is an
1475 empty list. When wantfiledata is true, filedata[i] is a triple (mode, size,
1476 mtime) of the file written for action[i].
1473 """ 1477 """
1478 filedata = []
1474 verbose = repo.ui.verbose 1479 verbose = repo.ui.verbose
1475 fctx = mctx.filectx 1480 fctx = mctx.filectx
1476 ui = repo.ui 1481 ui = repo.ui
1477 i = 0 1482 i = 0
1478 with repo.wvfs.backgroundclosing(ui, expectedcount=len(actions)): 1483 with repo.wvfs.backgroundclosing(ui, expectedcount=len(actions)):
1492 conflicting = p 1497 conflicting = p
1493 break 1498 break
1494 if repo.wvfs.lexists(conflicting): 1499 if repo.wvfs.lexists(conflicting):
1495 orig = scmutil.backuppath(ui, repo, conflicting) 1500 orig = scmutil.backuppath(ui, repo, conflicting)
1496 util.rename(repo.wjoin(conflicting), orig) 1501 util.rename(repo.wjoin(conflicting), orig)
1497 wctx[f].clearunknown() 1502 wfctx = wctx[f]
1503 wfctx.clearunknown()
1498 atomictemp = ui.configbool("experimental", "update.atomic-file") 1504 atomictemp = ui.configbool("experimental", "update.atomic-file")
1499 wctx[f].write(fctx(f).data(), flags, backgroundclose=True, 1505 size = wfctx.write(fctx(f).data(), flags,
1500 atomictemp=atomictemp) 1506 backgroundclose=True,
1507 atomictemp=atomictemp)
1508 if wantfiledata:
1509 s = wfctx.lstat()
1510 mode = s.st_mode
1511 mtime = s[stat.ST_MTIME]
1512 filedata.append((mode, size, mtime)) # for dirstate.normal
1501 if i == 100: 1513 if i == 100:
1502 yield i, f 1514 yield False, (i, f)
1503 i = 0 1515 i = 0
1504 i += 1 1516 i += 1
1505 if i > 0: 1517 if i > 0:
1506 yield i, f 1518 yield False, (i, f)
1519 yield True, filedata
1507 1520
1508 def _prefetchfiles(repo, ctx, actions): 1521 def _prefetchfiles(repo, ctx, actions):
1509 """Invoke ``scmutil.prefetchfiles()`` for the files relevant to the dict 1522 """Invoke ``scmutil.prefetchfiles()`` for the files relevant to the dict
1510 of merge actions. ``ctx`` is the context being merged in.""" 1523 of merge actions. ``ctx`` is the context being merged in."""
1511 1524
1548 ACTION_EXEC, 1561 ACTION_EXEC,
1549 ACTION_KEEP, 1562 ACTION_KEEP,
1550 ACTION_PATH_CONFLICT, 1563 ACTION_PATH_CONFLICT,
1551 ACTION_PATH_CONFLICT_RESOLVE)) 1564 ACTION_PATH_CONFLICT_RESOLVE))
1552 1565
1553 def applyupdates(repo, actions, wctx, mctx, overwrite, labels=None): 1566 def applyupdates(repo, actions, wctx, mctx, overwrite, wantfiledata,
1567 labels=None):
1554 """apply the merge action list to the working directory 1568 """apply the merge action list to the working directory
1555 1569
1556 wctx is the working copy context 1570 wctx is the working copy context
1557 mctx is the context to be merged into the working copy 1571 mctx is the context to be merged into the working copy
1558 1572
1559 Return a tuple of counts (updated, merged, removed, unresolved) that 1573 Return a tuple of (counts, filedata), where counts is a tuple
1560 describes how many files were affected by the update. 1574 (updated, merged, removed, unresolved) that describes how many
1575 files were affected by the update, and filedata is as described in
1576 batchget.
1561 """ 1577 """
1562 1578
1563 _prefetchfiles(repo, mctx, actions) 1579 _prefetchfiles(repo, mctx, actions)
1564 1580
1565 updated, merged, removed = 0, 0, 0 1581 updated, merged, removed = 0, 0, 0
1647 progress.increment(item=f) 1663 progress.increment(item=f)
1648 1664
1649 # get in parallel. 1665 # get in parallel.
1650 threadsafe = repo.ui.configbool('experimental', 1666 threadsafe = repo.ui.configbool('experimental',
1651 'worker.wdir-get-thread-safe') 1667 'worker.wdir-get-thread-safe')
1652 prog = worker.worker(repo.ui, cost, batchget, (repo, mctx, wctx), 1668 prog = worker.worker(repo.ui, cost, batchget,
1669 (repo, mctx, wctx, wantfiledata),
1653 actions[ACTION_GET], 1670 actions[ACTION_GET],
1654 threadsafe=threadsafe) 1671 threadsafe=threadsafe,
1655 for i, item in prog: 1672 hasretval=True)
1656 progress.increment(step=i, item=item) 1673 getfiledata = []
1674 for final, res in prog:
1675 if final:
1676 getfiledata = res
1677 else:
1678 i, item = res
1679 progress.increment(step=i, item=item)
1657 updated = len(actions[ACTION_GET]) 1680 updated = len(actions[ACTION_GET])
1658 1681
1659 if [a for a in actions[ACTION_GET] if a[0] == '.hgsubstate']: 1682 if [a for a in actions[ACTION_GET] if a[0] == '.hgsubstate']:
1660 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels) 1683 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1661 1684
1776 extraactions = ms.actions() 1799 extraactions = ms.actions()
1777 if extraactions: 1800 if extraactions:
1778 mfiles = set(a[0] for a in actions[ACTION_MERGE]) 1801 mfiles = set(a[0] for a in actions[ACTION_MERGE])
1779 for k, acts in extraactions.iteritems(): 1802 for k, acts in extraactions.iteritems():
1780 actions[k].extend(acts) 1803 actions[k].extend(acts)
1804 if k == ACTION_GET and wantfiledata:
1805 # no filedata until mergestate is updated to provide it
1806 getfiledata.extend([None] * len(acts))
1781 # Remove these files from actions[ACTION_MERGE] as well. This is 1807 # Remove these files from actions[ACTION_MERGE] as well. This is
1782 # important because in recordupdates, files in actions[ACTION_MERGE] 1808 # important because in recordupdates, files in actions[ACTION_MERGE]
1783 # are processed after files in other actions, and the merge driver 1809 # are processed after files in other actions, and the merge driver
1784 # might add files to those actions via extraactions above. This can 1810 # might add files to those actions via extraactions above. This can
1785 # lead to a file being recorded twice, with poor results. This is 1811 # lead to a file being recorded twice, with poor results. This is
1798 1824
1799 actions[ACTION_MERGE] = [a for a in actions[ACTION_MERGE] 1825 actions[ACTION_MERGE] = [a for a in actions[ACTION_MERGE]
1800 if a[0] in mfiles] 1826 if a[0] in mfiles]
1801 1827
1802 progress.complete() 1828 progress.complete()
1803 return updateresult(updated, merged, removed, unresolved) 1829 assert len(getfiledata) == (len(actions[ACTION_GET]) if wantfiledata else 0)
1804 1830 return updateresult(updated, merged, removed, unresolved), getfiledata
1805 def recordupdates(repo, actions, branchmerge): 1831
1832 def recordupdates(repo, actions, branchmerge, getfiledata):
1806 "record merge actions to the dirstate" 1833 "record merge actions to the dirstate"
1807 # remove (must come first) 1834 # remove (must come first)
1808 for f, args, msg in actions.get(ACTION_REMOVE, []): 1835 for f, args, msg in actions.get(ACTION_REMOVE, []):
1809 if branchmerge: 1836 if branchmerge:
1810 repo.dirstate.remove(f) 1837 repo.dirstate.remove(f)
1844 # keep 1871 # keep
1845 for f, args, msg in actions.get(ACTION_KEEP, []): 1872 for f, args, msg in actions.get(ACTION_KEEP, []):
1846 pass 1873 pass
1847 1874
1848 # get 1875 # get
1849 for f, args, msg in actions.get(ACTION_GET, []): 1876 for i, (f, args, msg) in enumerate(actions.get(ACTION_GET, [])):
1850 if branchmerge: 1877 if branchmerge:
1851 repo.dirstate.otherparent(f) 1878 repo.dirstate.otherparent(f)
1852 else: 1879 else:
1853 repo.dirstate.normal(f) 1880 parentfiledata = getfiledata[i] if getfiledata else None
1881 repo.dirstate.normal(f, parentfiledata=parentfiledata)
1854 1882
1855 # merge 1883 # merge
1856 for f, args, msg in actions.get(ACTION_MERGE, []): 1884 for f, args, msg in actions.get(ACTION_MERGE, []):
1857 f1, f2, fa, move, anc = args 1885 f1, f2, fa, move, anc = args
1858 if branchmerge: 1886 if branchmerge:
2164 repo.ui.warn( 2192 repo.ui.warn(
2165 _('(warning: large working directory being used without ' 2193 _('(warning: large working directory being used without '
2166 'fsmonitor enabled; enable fsmonitor to improve performance; ' 2194 'fsmonitor enabled; enable fsmonitor to improve performance; '
2167 'see "hg help -e fsmonitor")\n')) 2195 'see "hg help -e fsmonitor")\n'))
2168 2196
2169 stats = applyupdates(repo, actions, wc, p2, overwrite, labels=labels) 2197 updatedirstate = not partial and not wc.isinmemory()
2170 2198 wantfiledata = updatedirstate and not branchmerge
2171 if not partial and not wc.isinmemory(): 2199 stats, getfiledata = applyupdates(repo, actions, wc, p2, overwrite,
2200 wantfiledata, labels=labels)
2201
2202 if updatedirstate:
2172 with repo.dirstate.parentchange(): 2203 with repo.dirstate.parentchange():
2173 repo.setparents(fp1, fp2) 2204 repo.setparents(fp1, fp2)
2174 recordupdates(repo, actions, branchmerge) 2205 recordupdates(repo, actions, branchmerge, getfiledata)
2175 # update completed, clear state 2206 # update completed, clear state
2176 util.unlink(repo.vfs.join('updatestate')) 2207 util.unlink(repo.vfs.join('updatestate'))
2177 2208
2178 if not branchmerge: 2209 if not branchmerge:
2179 repo.dirstate.setbranch(p2.branch()) 2210 repo.dirstate.setbranch(p2.branch())