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