##// END OF EJS Templates
dirstate: split dirstatemap in its own file...
marmoute -
r48295:8b7e4780 default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (574 lines changed) Show them Hide them
@@ -19,6 +19,7 b' from .pycompat import delattr'
19 from hgdemandimport import tracing
19 from hgdemandimport import tracing
20
20
21 from . import (
21 from . import (
22 dirstatemap,
22 encoding,
23 encoding,
23 error,
24 error,
24 match as matchmod,
25 match as matchmod,
@@ -27,7 +28,6 b' from . import ('
27 pycompat,
28 pycompat,
28 scmutil,
29 scmutil,
29 sparse,
30 sparse,
30 txnutil,
31 util,
31 util,
32 )
32 )
33
33
@@ -49,13 +49,13 b' dirstatetuple = parsers.dirstatetuple'
49
49
50
50
51 # a special value used internally for `size` if the file come from the other parent
51 # a special value used internally for `size` if the file come from the other parent
52 FROM_P2 = -2
52 FROM_P2 = dirstatemap.FROM_P2
53
53
54 # a special value used internally for `size` if the file is modified/merged/added
54 # a special value used internally for `size` if the file is modified/merged/added
55 NONNORMAL = -1
55 NONNORMAL = dirstatemap.NONNORMAL
56
56
57 # a special value used internally for `time` if the time is ambigeous
57 # a special value used internally for `time` if the time is ambigeous
58 AMBIGUOUS_TIME = -1
58 AMBIGUOUS_TIME = dirstatemap.AMBIGUOUS_TIME
59
59
60
60
61 class repocache(filecache):
61 class repocache(filecache):
@@ -119,7 +119,7 b' class dirstate(object):'
119 self._plchangecallbacks = {}
119 self._plchangecallbacks = {}
120 self._origpl = None
120 self._origpl = None
121 self._updatedfiles = set()
121 self._updatedfiles = set()
122 self._mapcls = dirstatemap
122 self._mapcls = dirstatemap.dirstatemap
123 # Access and cache cwd early, so we don't access it for the first time
123 # Access and cache cwd early, so we don't access it for the first time
124 # after a working-copy update caused it to not exist (accessing it then
124 # after a working-copy update caused it to not exist (accessing it then
125 # raises an exception).
125 # raises an exception).
@@ -1450,567 +1450,3 b' class dirstate(object):'
1450 def clearbackup(self, tr, backupname):
1450 def clearbackup(self, tr, backupname):
1451 '''Clear backup file'''
1451 '''Clear backup file'''
1452 self._opener.unlink(backupname)
1452 self._opener.unlink(backupname)
1453
1454
1455 class dirstatemap(object):
1456 """Map encapsulating the dirstate's contents.
1457
1458 The dirstate contains the following state:
1459
1460 - `identity` is the identity of the dirstate file, which can be used to
1461 detect when changes have occurred to the dirstate file.
1462
1463 - `parents` is a pair containing the parents of the working copy. The
1464 parents are updated by calling `setparents`.
1465
1466 - the state map maps filenames to tuples of (state, mode, size, mtime),
1467 where state is a single character representing 'normal', 'added',
1468 'removed', or 'merged'. It is read by treating the dirstate as a
1469 dict. File state is updated by calling the `addfile`, `removefile` and
1470 `dropfile` methods.
1471
1472 - `copymap` maps destination filenames to their source filename.
1473
1474 The dirstate also provides the following views onto the state:
1475
1476 - `nonnormalset` is a set of the filenames that have state other
1477 than 'normal', or are normal but have an mtime of -1 ('normallookup').
1478
1479 - `otherparentset` is a set of the filenames that are marked as coming
1480 from the second parent when the dirstate is currently being merged.
1481
1482 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
1483 form that they appear as in the dirstate.
1484
1485 - `dirfoldmap` is a dict mapping normalized directory names to the
1486 denormalized form that they appear as in the dirstate.
1487 """
1488
1489 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
1490 self._ui = ui
1491 self._opener = opener
1492 self._root = root
1493 self._filename = b'dirstate'
1494 self._nodelen = 20
1495 self._nodeconstants = nodeconstants
1496 assert (
1497 not use_dirstate_v2
1498 ), "should have detected unsupported requirement"
1499
1500 self._parents = None
1501 self._dirtyparents = False
1502
1503 # for consistent view between _pl() and _read() invocations
1504 self._pendingmode = None
1505
1506 @propertycache
1507 def _map(self):
1508 self._map = {}
1509 self.read()
1510 return self._map
1511
1512 @propertycache
1513 def copymap(self):
1514 self.copymap = {}
1515 self._map
1516 return self.copymap
1517
1518 def directories(self):
1519 # Rust / dirstate-v2 only
1520 return []
1521
1522 def clear(self):
1523 self._map.clear()
1524 self.copymap.clear()
1525 self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
1526 util.clearcachedproperty(self, b"_dirs")
1527 util.clearcachedproperty(self, b"_alldirs")
1528 util.clearcachedproperty(self, b"filefoldmap")
1529 util.clearcachedproperty(self, b"dirfoldmap")
1530 util.clearcachedproperty(self, b"nonnormalset")
1531 util.clearcachedproperty(self, b"otherparentset")
1532
1533 def items(self):
1534 return pycompat.iteritems(self._map)
1535
1536 # forward for python2,3 compat
1537 iteritems = items
1538
1539 def __len__(self):
1540 return len(self._map)
1541
1542 def __iter__(self):
1543 return iter(self._map)
1544
1545 def get(self, key, default=None):
1546 return self._map.get(key, default)
1547
1548 def __contains__(self, key):
1549 return key in self._map
1550
1551 def __getitem__(self, key):
1552 return self._map[key]
1553
1554 def keys(self):
1555 return self._map.keys()
1556
1557 def preload(self):
1558 """Loads the underlying data, if it's not already loaded"""
1559 self._map
1560
1561 def addfile(self, f, oldstate, state, mode, size, mtime):
1562 """Add a tracked file to the dirstate."""
1563 if oldstate in b"?r" and "_dirs" in self.__dict__:
1564 self._dirs.addpath(f)
1565 if oldstate == b"?" and "_alldirs" in self.__dict__:
1566 self._alldirs.addpath(f)
1567 self._map[f] = dirstatetuple(state, mode, size, mtime)
1568 if state != b'n' or mtime == AMBIGUOUS_TIME:
1569 self.nonnormalset.add(f)
1570 if size == FROM_P2:
1571 self.otherparentset.add(f)
1572
1573 def removefile(self, f, oldstate, size):
1574 """
1575 Mark a file as removed in the dirstate.
1576
1577 The `size` parameter is used to store sentinel values that indicate
1578 the file's previous state. In the future, we should refactor this
1579 to be more explicit about what that state is.
1580 """
1581 if oldstate not in b"?r" and "_dirs" in self.__dict__:
1582 self._dirs.delpath(f)
1583 if oldstate == b"?" and "_alldirs" in self.__dict__:
1584 self._alldirs.addpath(f)
1585 if "filefoldmap" in self.__dict__:
1586 normed = util.normcase(f)
1587 self.filefoldmap.pop(normed, None)
1588 self._map[f] = dirstatetuple(b'r', 0, size, 0)
1589 self.nonnormalset.add(f)
1590
1591 def dropfile(self, f, oldstate):
1592 """
1593 Remove a file from the dirstate. Returns True if the file was
1594 previously recorded.
1595 """
1596 exists = self._map.pop(f, None) is not None
1597 if exists:
1598 if oldstate != b"r" and "_dirs" in self.__dict__:
1599 self._dirs.delpath(f)
1600 if "_alldirs" in self.__dict__:
1601 self._alldirs.delpath(f)
1602 if "filefoldmap" in self.__dict__:
1603 normed = util.normcase(f)
1604 self.filefoldmap.pop(normed, None)
1605 self.nonnormalset.discard(f)
1606 return exists
1607
1608 def clearambiguoustimes(self, files, now):
1609 for f in files:
1610 e = self.get(f)
1611 if e is not None and e[0] == b'n' and e[3] == now:
1612 self._map[f] = dirstatetuple(e[0], e[1], e[2], AMBIGUOUS_TIME)
1613 self.nonnormalset.add(f)
1614
1615 def nonnormalentries(self):
1616 '''Compute the nonnormal dirstate entries from the dmap'''
1617 try:
1618 return parsers.nonnormalotherparententries(self._map)
1619 except AttributeError:
1620 nonnorm = set()
1621 otherparent = set()
1622 for fname, e in pycompat.iteritems(self._map):
1623 if e[0] != b'n' or e[3] == AMBIGUOUS_TIME:
1624 nonnorm.add(fname)
1625 if e[0] == b'n' and e[2] == FROM_P2:
1626 otherparent.add(fname)
1627 return nonnorm, otherparent
1628
1629 @propertycache
1630 def filefoldmap(self):
1631 """Returns a dictionary mapping normalized case paths to their
1632 non-normalized versions.
1633 """
1634 try:
1635 makefilefoldmap = parsers.make_file_foldmap
1636 except AttributeError:
1637 pass
1638 else:
1639 return makefilefoldmap(
1640 self._map, util.normcasespec, util.normcasefallback
1641 )
1642
1643 f = {}
1644 normcase = util.normcase
1645 for name, s in pycompat.iteritems(self._map):
1646 if s[0] != b'r':
1647 f[normcase(name)] = name
1648 f[b'.'] = b'.' # prevents useless util.fspath() invocation
1649 return f
1650
1651 def hastrackeddir(self, d):
1652 """
1653 Returns True if the dirstate contains a tracked (not removed) file
1654 in this directory.
1655 """
1656 return d in self._dirs
1657
1658 def hasdir(self, d):
1659 """
1660 Returns True if the dirstate contains a file (tracked or removed)
1661 in this directory.
1662 """
1663 return d in self._alldirs
1664
1665 @propertycache
1666 def _dirs(self):
1667 return pathutil.dirs(self._map, b'r')
1668
1669 @propertycache
1670 def _alldirs(self):
1671 return pathutil.dirs(self._map)
1672
1673 def _opendirstatefile(self):
1674 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
1675 if self._pendingmode is not None and self._pendingmode != mode:
1676 fp.close()
1677 raise error.Abort(
1678 _(b'working directory state may be changed parallelly')
1679 )
1680 self._pendingmode = mode
1681 return fp
1682
1683 def parents(self):
1684 if not self._parents:
1685 try:
1686 fp = self._opendirstatefile()
1687 st = fp.read(2 * self._nodelen)
1688 fp.close()
1689 except IOError as err:
1690 if err.errno != errno.ENOENT:
1691 raise
1692 # File doesn't exist, so the current state is empty
1693 st = b''
1694
1695 l = len(st)
1696 if l == self._nodelen * 2:
1697 self._parents = (
1698 st[: self._nodelen],
1699 st[self._nodelen : 2 * self._nodelen],
1700 )
1701 elif l == 0:
1702 self._parents = (
1703 self._nodeconstants.nullid,
1704 self._nodeconstants.nullid,
1705 )
1706 else:
1707 raise error.Abort(
1708 _(b'working directory state appears damaged!')
1709 )
1710
1711 return self._parents
1712
1713 def setparents(self, p1, p2):
1714 self._parents = (p1, p2)
1715 self._dirtyparents = True
1716
1717 def read(self):
1718 # ignore HG_PENDING because identity is used only for writing
1719 self.identity = util.filestat.frompath(
1720 self._opener.join(self._filename)
1721 )
1722
1723 try:
1724 fp = self._opendirstatefile()
1725 try:
1726 st = fp.read()
1727 finally:
1728 fp.close()
1729 except IOError as err:
1730 if err.errno != errno.ENOENT:
1731 raise
1732 return
1733 if not st:
1734 return
1735
1736 if util.safehasattr(parsers, b'dict_new_presized'):
1737 # Make an estimate of the number of files in the dirstate based on
1738 # its size. This trades wasting some memory for avoiding costly
1739 # resizes. Each entry have a prefix of 17 bytes followed by one or
1740 # two path names. Studies on various large-scale real-world repositories
1741 # found 54 bytes a reasonable upper limit for the average path names.
1742 # Copy entries are ignored for the sake of this estimate.
1743 self._map = parsers.dict_new_presized(len(st) // 71)
1744
1745 # Python's garbage collector triggers a GC each time a certain number
1746 # of container objects (the number being defined by
1747 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
1748 # for each file in the dirstate. The C version then immediately marks
1749 # them as not to be tracked by the collector. However, this has no
1750 # effect on when GCs are triggered, only on what objects the GC looks
1751 # into. This means that O(number of files) GCs are unavoidable.
1752 # Depending on when in the process's lifetime the dirstate is parsed,
1753 # this can get very expensive. As a workaround, disable GC while
1754 # parsing the dirstate.
1755 #
1756 # (we cannot decorate the function directly since it is in a C module)
1757 parse_dirstate = util.nogc(parsers.parse_dirstate)
1758 p = parse_dirstate(self._map, self.copymap, st)
1759 if not self._dirtyparents:
1760 self.setparents(*p)
1761
1762 # Avoid excess attribute lookups by fast pathing certain checks
1763 self.__contains__ = self._map.__contains__
1764 self.__getitem__ = self._map.__getitem__
1765 self.get = self._map.get
1766
1767 def write(self, st, now):
1768 st.write(
1769 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
1770 )
1771 st.close()
1772 self._dirtyparents = False
1773 self.nonnormalset, self.otherparentset = self.nonnormalentries()
1774
1775 @propertycache
1776 def nonnormalset(self):
1777 nonnorm, otherparents = self.nonnormalentries()
1778 self.otherparentset = otherparents
1779 return nonnorm
1780
1781 @propertycache
1782 def otherparentset(self):
1783 nonnorm, otherparents = self.nonnormalentries()
1784 self.nonnormalset = nonnorm
1785 return otherparents
1786
1787 def non_normal_or_other_parent_paths(self):
1788 return self.nonnormalset.union(self.otherparentset)
1789
1790 @propertycache
1791 def identity(self):
1792 self._map
1793 return self.identity
1794
1795 @propertycache
1796 def dirfoldmap(self):
1797 f = {}
1798 normcase = util.normcase
1799 for name in self._dirs:
1800 f[normcase(name)] = name
1801 return f
1802
1803
1804 if rustmod is not None:
1805
1806 class dirstatemap(object):
1807 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
1808 self._use_dirstate_v2 = use_dirstate_v2
1809 self._nodeconstants = nodeconstants
1810 self._ui = ui
1811 self._opener = opener
1812 self._root = root
1813 self._filename = b'dirstate'
1814 self._nodelen = 20 # Also update Rust code when changing this!
1815 self._parents = None
1816 self._dirtyparents = False
1817
1818 # for consistent view between _pl() and _read() invocations
1819 self._pendingmode = None
1820
1821 self._use_dirstate_tree = self._ui.configbool(
1822 b"experimental",
1823 b"dirstate-tree.in-memory",
1824 False,
1825 )
1826
1827 def addfile(self, *args, **kwargs):
1828 return self._rustmap.addfile(*args, **kwargs)
1829
1830 def removefile(self, *args, **kwargs):
1831 return self._rustmap.removefile(*args, **kwargs)
1832
1833 def dropfile(self, *args, **kwargs):
1834 return self._rustmap.dropfile(*args, **kwargs)
1835
1836 def clearambiguoustimes(self, *args, **kwargs):
1837 return self._rustmap.clearambiguoustimes(*args, **kwargs)
1838
1839 def nonnormalentries(self):
1840 return self._rustmap.nonnormalentries()
1841
1842 def get(self, *args, **kwargs):
1843 return self._rustmap.get(*args, **kwargs)
1844
1845 @property
1846 def copymap(self):
1847 return self._rustmap.copymap()
1848
1849 def directories(self):
1850 return self._rustmap.directories()
1851
1852 def preload(self):
1853 self._rustmap
1854
1855 def clear(self):
1856 self._rustmap.clear()
1857 self.setparents(
1858 self._nodeconstants.nullid, self._nodeconstants.nullid
1859 )
1860 util.clearcachedproperty(self, b"_dirs")
1861 util.clearcachedproperty(self, b"_alldirs")
1862 util.clearcachedproperty(self, b"dirfoldmap")
1863
1864 def items(self):
1865 return self._rustmap.items()
1866
1867 def keys(self):
1868 return iter(self._rustmap)
1869
1870 def __contains__(self, key):
1871 return key in self._rustmap
1872
1873 def __getitem__(self, item):
1874 return self._rustmap[item]
1875
1876 def __len__(self):
1877 return len(self._rustmap)
1878
1879 def __iter__(self):
1880 return iter(self._rustmap)
1881
1882 # forward for python2,3 compat
1883 iteritems = items
1884
1885 def _opendirstatefile(self):
1886 fp, mode = txnutil.trypending(
1887 self._root, self._opener, self._filename
1888 )
1889 if self._pendingmode is not None and self._pendingmode != mode:
1890 fp.close()
1891 raise error.Abort(
1892 _(b'working directory state may be changed parallelly')
1893 )
1894 self._pendingmode = mode
1895 return fp
1896
1897 def setparents(self, p1, p2):
1898 self._parents = (p1, p2)
1899 self._dirtyparents = True
1900
1901 def parents(self):
1902 if not self._parents:
1903 if self._use_dirstate_v2:
1904 offset = len(rustmod.V2_FORMAT_MARKER)
1905 else:
1906 offset = 0
1907 read_len = offset + self._nodelen * 2
1908 try:
1909 fp = self._opendirstatefile()
1910 st = fp.read(read_len)
1911 fp.close()
1912 except IOError as err:
1913 if err.errno != errno.ENOENT:
1914 raise
1915 # File doesn't exist, so the current state is empty
1916 st = b''
1917
1918 l = len(st)
1919 if l == read_len:
1920 st = st[offset:]
1921 self._parents = (
1922 st[: self._nodelen],
1923 st[self._nodelen : 2 * self._nodelen],
1924 )
1925 elif l == 0:
1926 self._parents = (
1927 self._nodeconstants.nullid,
1928 self._nodeconstants.nullid,
1929 )
1930 else:
1931 raise error.Abort(
1932 _(b'working directory state appears damaged!')
1933 )
1934
1935 return self._parents
1936
1937 @propertycache
1938 def _rustmap(self):
1939 """
1940 Fills the Dirstatemap when called.
1941 """
1942 # ignore HG_PENDING because identity is used only for writing
1943 self.identity = util.filestat.frompath(
1944 self._opener.join(self._filename)
1945 )
1946
1947 try:
1948 fp = self._opendirstatefile()
1949 try:
1950 st = fp.read()
1951 finally:
1952 fp.close()
1953 except IOError as err:
1954 if err.errno != errno.ENOENT:
1955 raise
1956 st = b''
1957
1958 self._rustmap, parents = rustmod.DirstateMap.new(
1959 self._use_dirstate_tree, self._use_dirstate_v2, st
1960 )
1961
1962 if parents and not self._dirtyparents:
1963 self.setparents(*parents)
1964
1965 self.__contains__ = self._rustmap.__contains__
1966 self.__getitem__ = self._rustmap.__getitem__
1967 self.get = self._rustmap.get
1968 return self._rustmap
1969
1970 def write(self, st, now):
1971 parents = self.parents()
1972 packed = self._rustmap.write(
1973 self._use_dirstate_v2, parents[0], parents[1], now
1974 )
1975 st.write(packed)
1976 st.close()
1977 self._dirtyparents = False
1978
1979 @propertycache
1980 def filefoldmap(self):
1981 """Returns a dictionary mapping normalized case paths to their
1982 non-normalized versions.
1983 """
1984 return self._rustmap.filefoldmapasdict()
1985
1986 def hastrackeddir(self, d):
1987 return self._rustmap.hastrackeddir(d)
1988
1989 def hasdir(self, d):
1990 return self._rustmap.hasdir(d)
1991
1992 @propertycache
1993 def identity(self):
1994 self._rustmap
1995 return self.identity
1996
1997 @property
1998 def nonnormalset(self):
1999 nonnorm = self._rustmap.non_normal_entries()
2000 return nonnorm
2001
2002 @propertycache
2003 def otherparentset(self):
2004 otherparents = self._rustmap.other_parent_entries()
2005 return otherparents
2006
2007 def non_normal_or_other_parent_paths(self):
2008 return self._rustmap.non_normal_or_other_parent_paths()
2009
2010 @propertycache
2011 def dirfoldmap(self):
2012 f = {}
2013 normcase = util.normcase
2014 for name, _pseudo_entry in self.directories():
2015 f[normcase(name)] = name
2016 return f
This diff has been collapsed as it changes many lines, (1418 lines changed) Show them Hide them
@@ -1,49 +1,27 b''
1 # dirstate.py - working directory tracking for mercurial
1 # dirstatemap.py
2 #
3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 #
2 #
5 # This software may be used and distributed according to the terms of the
3 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
4 # GNU General Public License version 2 or any later version.
7
5
8 from __future__ import absolute_import
6 from __future__ import absolute_import
9
7
10 import collections
11 import contextlib
12 import errno
8 import errno
13 import os
14 import stat
15
9
16 from .i18n import _
10 from .i18n import _
17 from .pycompat import delattr
18
19 from hgdemandimport import tracing
20
11
21 from . import (
12 from . import (
22 encoding,
23 error,
13 error,
24 match as matchmod,
25 pathutil,
14 pathutil,
26 policy,
15 policy,
27 pycompat,
16 pycompat,
28 scmutil,
29 sparse,
30 txnutil,
17 txnutil,
31 util,
18 util,
32 )
19 )
33
20
34 from .interfaces import (
35 dirstate as intdirstate,
36 util as interfaceutil,
37 )
38
39 parsers = policy.importmod('parsers')
21 parsers = policy.importmod('parsers')
40 rustmod = policy.importrust('dirstate')
22 rustmod = policy.importrust('dirstate')
41
23
42 SUPPORTS_DIRSTATE_V2 = rustmod is not None
43
44 propertycache = util.propertycache
24 propertycache = util.propertycache
45 filecache = scmutil.filecache
46 _rangemask = 0x7FFFFFFF
47
25
48 dirstatetuple = parsers.dirstatetuple
26 dirstatetuple = parsers.dirstatetuple
49
27
@@ -58,1400 +36,6 b' NONNORMAL = -1'
58 AMBIGUOUS_TIME = -1
36 AMBIGUOUS_TIME = -1
59
37
60
38
61 class repocache(filecache):
62 """filecache for files in .hg/"""
63
64 def join(self, obj, fname):
65 return obj._opener.join(fname)
66
67
68 class rootcache(filecache):
69 """filecache for files in the repository root"""
70
71 def join(self, obj, fname):
72 return obj._join(fname)
73
74
75 def _getfsnow(vfs):
76 '''Get "now" timestamp on filesystem'''
77 tmpfd, tmpname = vfs.mkstemp()
78 try:
79 return os.fstat(tmpfd)[stat.ST_MTIME]
80 finally:
81 os.close(tmpfd)
82 vfs.unlink(tmpname)
83
84
85 @interfaceutil.implementer(intdirstate.idirstate)
86 class dirstate(object):
87 def __init__(
88 self,
89 opener,
90 ui,
91 root,
92 validate,
93 sparsematchfn,
94 nodeconstants,
95 use_dirstate_v2,
96 ):
97 """Create a new dirstate object.
98
99 opener is an open()-like callable that can be used to open the
100 dirstate file; root is the root of the directory tracked by
101 the dirstate.
102 """
103 self._use_dirstate_v2 = use_dirstate_v2
104 self._nodeconstants = nodeconstants
105 self._opener = opener
106 self._validate = validate
107 self._root = root
108 self._sparsematchfn = sparsematchfn
109 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
110 # UNC path pointing to root share (issue4557)
111 self._rootdir = pathutil.normasprefix(root)
112 self._dirty = False
113 self._lastnormaltime = 0
114 self._ui = ui
115 self._filecache = {}
116 self._parentwriters = 0
117 self._filename = b'dirstate'
118 self._pendingfilename = b'%s.pending' % self._filename
119 self._plchangecallbacks = {}
120 self._origpl = None
121 self._updatedfiles = set()
122 self._mapcls = dirstatemap
123 # Access and cache cwd early, so we don't access it for the first time
124 # after a working-copy update caused it to not exist (accessing it then
125 # raises an exception).
126 self._cwd
127
128 def prefetch_parents(self):
129 """make sure the parents are loaded
130
131 Used to avoid a race condition.
132 """
133 self._pl
134
135 @contextlib.contextmanager
136 def parentchange(self):
137 """Context manager for handling dirstate parents.
138
139 If an exception occurs in the scope of the context manager,
140 the incoherent dirstate won't be written when wlock is
141 released.
142 """
143 self._parentwriters += 1
144 yield
145 # Typically we want the "undo" step of a context manager in a
146 # finally block so it happens even when an exception
147 # occurs. In this case, however, we only want to decrement
148 # parentwriters if the code in the with statement exits
149 # normally, so we don't have a try/finally here on purpose.
150 self._parentwriters -= 1
151
152 def pendingparentchange(self):
153 """Returns true if the dirstate is in the middle of a set of changes
154 that modify the dirstate parent.
155 """
156 return self._parentwriters > 0
157
158 @propertycache
159 def _map(self):
160 """Return the dirstate contents (see documentation for dirstatemap)."""
161 self._map = self._mapcls(
162 self._ui,
163 self._opener,
164 self._root,
165 self._nodeconstants,
166 self._use_dirstate_v2,
167 )
168 return self._map
169
170 @property
171 def _sparsematcher(self):
172 """The matcher for the sparse checkout.
173
174 The working directory may not include every file from a manifest. The
175 matcher obtained by this property will match a path if it is to be
176 included in the working directory.
177 """
178 # TODO there is potential to cache this property. For now, the matcher
179 # is resolved on every access. (But the called function does use a
180 # cache to keep the lookup fast.)
181 return self._sparsematchfn()
182
183 @repocache(b'branch')
184 def _branch(self):
185 try:
186 return self._opener.read(b"branch").strip() or b"default"
187 except IOError as inst:
188 if inst.errno != errno.ENOENT:
189 raise
190 return b"default"
191
192 @property
193 def _pl(self):
194 return self._map.parents()
195
196 def hasdir(self, d):
197 return self._map.hastrackeddir(d)
198
199 @rootcache(b'.hgignore')
200 def _ignore(self):
201 files = self._ignorefiles()
202 if not files:
203 return matchmod.never()
204
205 pats = [b'include:%s' % f for f in files]
206 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
207
208 @propertycache
209 def _slash(self):
210 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
211
212 @propertycache
213 def _checklink(self):
214 return util.checklink(self._root)
215
216 @propertycache
217 def _checkexec(self):
218 return bool(util.checkexec(self._root))
219
220 @propertycache
221 def _checkcase(self):
222 return not util.fscasesensitive(self._join(b'.hg'))
223
224 def _join(self, f):
225 # much faster than os.path.join()
226 # it's safe because f is always a relative path
227 return self._rootdir + f
228
229 def flagfunc(self, buildfallback):
230 if self._checklink and self._checkexec:
231
232 def f(x):
233 try:
234 st = os.lstat(self._join(x))
235 if util.statislink(st):
236 return b'l'
237 if util.statisexec(st):
238 return b'x'
239 except OSError:
240 pass
241 return b''
242
243 return f
244
245 fallback = buildfallback()
246 if self._checklink:
247
248 def f(x):
249 if os.path.islink(self._join(x)):
250 return b'l'
251 if b'x' in fallback(x):
252 return b'x'
253 return b''
254
255 return f
256 if self._checkexec:
257
258 def f(x):
259 if b'l' in fallback(x):
260 return b'l'
261 if util.isexec(self._join(x)):
262 return b'x'
263 return b''
264
265 return f
266 else:
267 return fallback
268
269 @propertycache
270 def _cwd(self):
271 # internal config: ui.forcecwd
272 forcecwd = self._ui.config(b'ui', b'forcecwd')
273 if forcecwd:
274 return forcecwd
275 return encoding.getcwd()
276
277 def getcwd(self):
278 """Return the path from which a canonical path is calculated.
279
280 This path should be used to resolve file patterns or to convert
281 canonical paths back to file paths for display. It shouldn't be
282 used to get real file paths. Use vfs functions instead.
283 """
284 cwd = self._cwd
285 if cwd == self._root:
286 return b''
287 # self._root ends with a path separator if self._root is '/' or 'C:\'
288 rootsep = self._root
289 if not util.endswithsep(rootsep):
290 rootsep += pycompat.ossep
291 if cwd.startswith(rootsep):
292 return cwd[len(rootsep) :]
293 else:
294 # we're outside the repo. return an absolute path.
295 return cwd
296
297 def pathto(self, f, cwd=None):
298 if cwd is None:
299 cwd = self.getcwd()
300 path = util.pathto(self._root, cwd, f)
301 if self._slash:
302 return util.pconvert(path)
303 return path
304
305 def __getitem__(self, key):
306 """Return the current state of key (a filename) in the dirstate.
307
308 States are:
309 n normal
310 m needs merging
311 r marked for removal
312 a marked for addition
313 ? not tracked
314 """
315 return self._map.get(key, (b"?",))[0]
316
317 def __contains__(self, key):
318 return key in self._map
319
320 def __iter__(self):
321 return iter(sorted(self._map))
322
323 def items(self):
324 return pycompat.iteritems(self._map)
325
326 iteritems = items
327
328 def directories(self):
329 return self._map.directories()
330
331 def parents(self):
332 return [self._validate(p) for p in self._pl]
333
334 def p1(self):
335 return self._validate(self._pl[0])
336
337 def p2(self):
338 return self._validate(self._pl[1])
339
340 def branch(self):
341 return encoding.tolocal(self._branch)
342
343 def setparents(self, p1, p2=None):
344 """Set dirstate parents to p1 and p2.
345
346 When moving from two parents to one, 'm' merged entries a
347 adjusted to normal and previous copy records discarded and
348 returned by the call.
349
350 See localrepo.setparents()
351 """
352 if p2 is None:
353 p2 = self._nodeconstants.nullid
354 if self._parentwriters == 0:
355 raise ValueError(
356 b"cannot set dirstate parent outside of "
357 b"dirstate.parentchange context manager"
358 )
359
360 self._dirty = True
361 oldp2 = self._pl[1]
362 if self._origpl is None:
363 self._origpl = self._pl
364 self._map.setparents(p1, p2)
365 copies = {}
366 if (
367 oldp2 != self._nodeconstants.nullid
368 and p2 == self._nodeconstants.nullid
369 ):
370 candidatefiles = self._map.non_normal_or_other_parent_paths()
371
372 for f in candidatefiles:
373 s = self._map.get(f)
374 if s is None:
375 continue
376
377 # Discard 'm' markers when moving away from a merge state
378 if s[0] == b'm':
379 source = self._map.copymap.get(f)
380 if source:
381 copies[f] = source
382 self.normallookup(f)
383 # Also fix up otherparent markers
384 elif s[0] == b'n' and s[2] == FROM_P2:
385 source = self._map.copymap.get(f)
386 if source:
387 copies[f] = source
388 self.add(f)
389 return copies
390
391 def setbranch(self, branch):
392 self.__class__._branch.set(self, encoding.fromlocal(branch))
393 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
394 try:
395 f.write(self._branch + b'\n')
396 f.close()
397
398 # make sure filecache has the correct stat info for _branch after
399 # replacing the underlying file
400 ce = self._filecache[b'_branch']
401 if ce:
402 ce.refresh()
403 except: # re-raises
404 f.discard()
405 raise
406
407 def invalidate(self):
408 """Causes the next access to reread the dirstate.
409
410 This is different from localrepo.invalidatedirstate() because it always
411 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
412 check whether the dirstate has changed before rereading it."""
413
414 for a in ("_map", "_branch", "_ignore"):
415 if a in self.__dict__:
416 delattr(self, a)
417 self._lastnormaltime = 0
418 self._dirty = False
419 self._updatedfiles.clear()
420 self._parentwriters = 0
421 self._origpl = None
422
423 def copy(self, source, dest):
424 """Mark dest as a copy of source. Unmark dest if source is None."""
425 if source == dest:
426 return
427 self._dirty = True
428 if source is not None:
429 self._map.copymap[dest] = source
430 self._updatedfiles.add(source)
431 self._updatedfiles.add(dest)
432 elif self._map.copymap.pop(dest, None):
433 self._updatedfiles.add(dest)
434
435 def copied(self, file):
436 return self._map.copymap.get(file, None)
437
438 def copies(self):
439 return self._map.copymap
440
441 def _addpath(
442 self,
443 f,
444 state,
445 mode,
446 size=NONNORMAL,
447 mtime=AMBIGUOUS_TIME,
448 from_p2=False,
449 possibly_dirty=False,
450 ):
451 oldstate = self[f]
452 if state == b'a' or oldstate == b'r':
453 scmutil.checkfilename(f)
454 if self._map.hastrackeddir(f):
455 msg = _(b'directory %r already in dirstate')
456 msg %= pycompat.bytestr(f)
457 raise error.Abort(msg)
458 # shadows
459 for d in pathutil.finddirs(f):
460 if self._map.hastrackeddir(d):
461 break
462 entry = self._map.get(d)
463 if entry is not None and entry[0] != b'r':
464 msg = _(b'file %r in dirstate clashes with %r')
465 msg %= (pycompat.bytestr(d), pycompat.bytestr(f))
466 raise error.Abort(msg)
467 if state == b'a':
468 assert not possibly_dirty
469 assert not from_p2
470 size = NONNORMAL
471 mtime = AMBIGUOUS_TIME
472 elif from_p2:
473 assert not possibly_dirty
474 size = FROM_P2
475 mtime = AMBIGUOUS_TIME
476 elif possibly_dirty:
477 mtime = AMBIGUOUS_TIME
478 else:
479 assert size != FROM_P2
480 assert size != NONNORMAL
481 size = size & _rangemask
482 mtime = mtime & _rangemask
483 self._dirty = True
484 self._updatedfiles.add(f)
485 self._map.addfile(f, oldstate, state, mode, size, mtime)
486
487 def normal(self, f, parentfiledata=None):
488 """Mark a file normal and clean.
489
490 parentfiledata: (mode, size, mtime) of the clean file
491
492 parentfiledata should be computed from memory (for mode,
493 size), as or close as possible from the point where we
494 determined the file was clean, to limit the risk of the
495 file having been changed by an external process between the
496 moment where the file was determined to be clean and now."""
497 if parentfiledata:
498 (mode, size, mtime) = parentfiledata
499 else:
500 s = os.lstat(self._join(f))
501 mode = s.st_mode
502 size = s.st_size
503 mtime = s[stat.ST_MTIME]
504 self._addpath(f, b'n', mode, size, mtime)
505 self._map.copymap.pop(f, None)
506 if f in self._map.nonnormalset:
507 self._map.nonnormalset.remove(f)
508 if mtime > self._lastnormaltime:
509 # Remember the most recent modification timeslot for status(),
510 # to make sure we won't miss future size-preserving file content
511 # modifications that happen within the same timeslot.
512 self._lastnormaltime = mtime
513
514 def normallookup(self, f):
515 '''Mark a file normal, but possibly dirty.'''
516 if self._pl[1] != self._nodeconstants.nullid:
517 # if there is a merge going on and the file was either
518 # in state 'm' (-1) or coming from other parent (-2) before
519 # being removed, restore that state.
520 entry = self._map.get(f)
521 if entry is not None:
522 if entry[0] == b'r' and entry[2] in (NONNORMAL, FROM_P2):
523 source = self._map.copymap.get(f)
524 if entry[2] == NONNORMAL:
525 self.merge(f)
526 elif entry[2] == FROM_P2:
527 self.otherparent(f)
528 if source:
529 self.copy(source, f)
530 return
531 if entry[0] == b'm' or entry[0] == b'n' and entry[2] == FROM_P2:
532 return
533 self._addpath(f, b'n', 0, possibly_dirty=True)
534 self._map.copymap.pop(f, None)
535
536 def otherparent(self, f):
537 '''Mark as coming from the other parent, always dirty.'''
538 if self._pl[1] == self._nodeconstants.nullid:
539 msg = _(b"setting %r to other parent only allowed in merges") % f
540 raise error.Abort(msg)
541 if f in self and self[f] == b'n':
542 # merge-like
543 self._addpath(f, b'm', 0, from_p2=True)
544 else:
545 # add-like
546 self._addpath(f, b'n', 0, from_p2=True)
547 self._map.copymap.pop(f, None)
548
549 def add(self, f):
550 '''Mark a file added.'''
551 self._addpath(f, b'a', 0)
552 self._map.copymap.pop(f, None)
553
554 def remove(self, f):
555 '''Mark a file removed.'''
556 self._dirty = True
557 oldstate = self[f]
558 size = 0
559 if self._pl[1] != self._nodeconstants.nullid:
560 entry = self._map.get(f)
561 if entry is not None:
562 # backup the previous state
563 if entry[0] == b'm': # merge
564 size = NONNORMAL
565 elif entry[0] == b'n' and entry[2] == FROM_P2: # other parent
566 size = FROM_P2
567 self._map.otherparentset.add(f)
568 self._updatedfiles.add(f)
569 self._map.removefile(f, oldstate, size)
570 if size == 0:
571 self._map.copymap.pop(f, None)
572
573 def merge(self, f):
574 '''Mark a file merged.'''
575 if self._pl[1] == self._nodeconstants.nullid:
576 return self.normallookup(f)
577 return self.otherparent(f)
578
579 def drop(self, f):
580 '''Drop a file from the dirstate'''
581 oldstate = self[f]
582 if self._map.dropfile(f, oldstate):
583 self._dirty = True
584 self._updatedfiles.add(f)
585 self._map.copymap.pop(f, None)
586
587 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
588 if exists is None:
589 exists = os.path.lexists(os.path.join(self._root, path))
590 if not exists:
591 # Maybe a path component exists
592 if not ignoremissing and b'/' in path:
593 d, f = path.rsplit(b'/', 1)
594 d = self._normalize(d, False, ignoremissing, None)
595 folded = d + b"/" + f
596 else:
597 # No path components, preserve original case
598 folded = path
599 else:
600 # recursively normalize leading directory components
601 # against dirstate
602 if b'/' in normed:
603 d, f = normed.rsplit(b'/', 1)
604 d = self._normalize(d, False, ignoremissing, True)
605 r = self._root + b"/" + d
606 folded = d + b"/" + util.fspath(f, r)
607 else:
608 folded = util.fspath(normed, self._root)
609 storemap[normed] = folded
610
611 return folded
612
613 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
614 normed = util.normcase(path)
615 folded = self._map.filefoldmap.get(normed, None)
616 if folded is None:
617 if isknown:
618 folded = path
619 else:
620 folded = self._discoverpath(
621 path, normed, ignoremissing, exists, self._map.filefoldmap
622 )
623 return folded
624
625 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
626 normed = util.normcase(path)
627 folded = self._map.filefoldmap.get(normed, None)
628 if folded is None:
629 folded = self._map.dirfoldmap.get(normed, None)
630 if folded is None:
631 if isknown:
632 folded = path
633 else:
634 # store discovered result in dirfoldmap so that future
635 # normalizefile calls don't start matching directories
636 folded = self._discoverpath(
637 path, normed, ignoremissing, exists, self._map.dirfoldmap
638 )
639 return folded
640
641 def normalize(self, path, isknown=False, ignoremissing=False):
642 """
643 normalize the case of a pathname when on a casefolding filesystem
644
645 isknown specifies whether the filename came from walking the
646 disk, to avoid extra filesystem access.
647
648 If ignoremissing is True, missing path are returned
649 unchanged. Otherwise, we try harder to normalize possibly
650 existing path components.
651
652 The normalized case is determined based on the following precedence:
653
654 - version of name already stored in the dirstate
655 - version of name stored on disk
656 - version provided via command arguments
657 """
658
659 if self._checkcase:
660 return self._normalize(path, isknown, ignoremissing)
661 return path
662
663 def clear(self):
664 self._map.clear()
665 self._lastnormaltime = 0
666 self._updatedfiles.clear()
667 self._dirty = True
668
669 def rebuild(self, parent, allfiles, changedfiles=None):
670 if changedfiles is None:
671 # Rebuild entire dirstate
672 to_lookup = allfiles
673 to_drop = []
674 lastnormaltime = self._lastnormaltime
675 self.clear()
676 self._lastnormaltime = lastnormaltime
677 elif len(changedfiles) < 10:
678 # Avoid turning allfiles into a set, which can be expensive if it's
679 # large.
680 to_lookup = []
681 to_drop = []
682 for f in changedfiles:
683 if f in allfiles:
684 to_lookup.append(f)
685 else:
686 to_drop.append(f)
687 else:
688 changedfilesset = set(changedfiles)
689 to_lookup = changedfilesset & set(allfiles)
690 to_drop = changedfilesset - to_lookup
691
692 if self._origpl is None:
693 self._origpl = self._pl
694 self._map.setparents(parent, self._nodeconstants.nullid)
695
696 for f in to_lookup:
697 self.normallookup(f)
698 for f in to_drop:
699 self.drop(f)
700
701 self._dirty = True
702
703 def identity(self):
704 """Return identity of dirstate itself to detect changing in storage
705
706 If identity of previous dirstate is equal to this, writing
707 changes based on the former dirstate out can keep consistency.
708 """
709 return self._map.identity
710
711 def write(self, tr):
712 if not self._dirty:
713 return
714
715 filename = self._filename
716 if tr:
717 # 'dirstate.write()' is not only for writing in-memory
718 # changes out, but also for dropping ambiguous timestamp.
719 # delayed writing re-raise "ambiguous timestamp issue".
720 # See also the wiki page below for detail:
721 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
722
723 # emulate dropping timestamp in 'parsers.pack_dirstate'
724 now = _getfsnow(self._opener)
725 self._map.clearambiguoustimes(self._updatedfiles, now)
726
727 # emulate that all 'dirstate.normal' results are written out
728 self._lastnormaltime = 0
729 self._updatedfiles.clear()
730
731 # delay writing in-memory changes out
732 tr.addfilegenerator(
733 b'dirstate',
734 (self._filename,),
735 self._writedirstate,
736 location=b'plain',
737 )
738 return
739
740 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
741 self._writedirstate(st)
742
743 def addparentchangecallback(self, category, callback):
744 """add a callback to be called when the wd parents are changed
745
746 Callback will be called with the following arguments:
747 dirstate, (oldp1, oldp2), (newp1, newp2)
748
749 Category is a unique identifier to allow overwriting an old callback
750 with a newer callback.
751 """
752 self._plchangecallbacks[category] = callback
753
754 def _writedirstate(self, st):
755 # notify callbacks about parents change
756 if self._origpl is not None and self._origpl != self._pl:
757 for c, callback in sorted(
758 pycompat.iteritems(self._plchangecallbacks)
759 ):
760 callback(self, self._origpl, self._pl)
761 self._origpl = None
762 # use the modification time of the newly created temporary file as the
763 # filesystem's notion of 'now'
764 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
765
766 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
767 # timestamp of each entries in dirstate, because of 'now > mtime'
768 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
769 if delaywrite > 0:
770 # do we have any files to delay for?
771 for f, e in pycompat.iteritems(self._map):
772 if e[0] == b'n' and e[3] == now:
773 import time # to avoid useless import
774
775 # rather than sleep n seconds, sleep until the next
776 # multiple of n seconds
777 clock = time.time()
778 start = int(clock) - (int(clock) % delaywrite)
779 end = start + delaywrite
780 time.sleep(end - clock)
781 now = end # trust our estimate that the end is near now
782 break
783
784 self._map.write(st, now)
785 self._lastnormaltime = 0
786 self._dirty = False
787
788 def _dirignore(self, f):
789 if self._ignore(f):
790 return True
791 for p in pathutil.finddirs(f):
792 if self._ignore(p):
793 return True
794 return False
795
796 def _ignorefiles(self):
797 files = []
798 if os.path.exists(self._join(b'.hgignore')):
799 files.append(self._join(b'.hgignore'))
800 for name, path in self._ui.configitems(b"ui"):
801 if name == b'ignore' or name.startswith(b'ignore.'):
802 # we need to use os.path.join here rather than self._join
803 # because path is arbitrary and user-specified
804 files.append(os.path.join(self._rootdir, util.expandpath(path)))
805 return files
806
807 def _ignorefileandline(self, f):
808 files = collections.deque(self._ignorefiles())
809 visited = set()
810 while files:
811 i = files.popleft()
812 patterns = matchmod.readpatternfile(
813 i, self._ui.warn, sourceinfo=True
814 )
815 for pattern, lineno, line in patterns:
816 kind, p = matchmod._patsplit(pattern, b'glob')
817 if kind == b"subinclude":
818 if p not in visited:
819 files.append(p)
820 continue
821 m = matchmod.match(
822 self._root, b'', [], [pattern], warn=self._ui.warn
823 )
824 if m(f):
825 return (i, lineno, line)
826 visited.add(i)
827 return (None, -1, b"")
828
829 def _walkexplicit(self, match, subrepos):
830 """Get stat data about the files explicitly specified by match.
831
832 Return a triple (results, dirsfound, dirsnotfound).
833 - results is a mapping from filename to stat result. It also contains
834 listings mapping subrepos and .hg to None.
835 - dirsfound is a list of files found to be directories.
836 - dirsnotfound is a list of files that the dirstate thinks are
837 directories and that were not found."""
838
839 def badtype(mode):
840 kind = _(b'unknown')
841 if stat.S_ISCHR(mode):
842 kind = _(b'character device')
843 elif stat.S_ISBLK(mode):
844 kind = _(b'block device')
845 elif stat.S_ISFIFO(mode):
846 kind = _(b'fifo')
847 elif stat.S_ISSOCK(mode):
848 kind = _(b'socket')
849 elif stat.S_ISDIR(mode):
850 kind = _(b'directory')
851 return _(b'unsupported file type (type is %s)') % kind
852
853 badfn = match.bad
854 dmap = self._map
855 lstat = os.lstat
856 getkind = stat.S_IFMT
857 dirkind = stat.S_IFDIR
858 regkind = stat.S_IFREG
859 lnkkind = stat.S_IFLNK
860 join = self._join
861 dirsfound = []
862 foundadd = dirsfound.append
863 dirsnotfound = []
864 notfoundadd = dirsnotfound.append
865
866 if not match.isexact() and self._checkcase:
867 normalize = self._normalize
868 else:
869 normalize = None
870
871 files = sorted(match.files())
872 subrepos.sort()
873 i, j = 0, 0
874 while i < len(files) and j < len(subrepos):
875 subpath = subrepos[j] + b"/"
876 if files[i] < subpath:
877 i += 1
878 continue
879 while i < len(files) and files[i].startswith(subpath):
880 del files[i]
881 j += 1
882
883 if not files or b'' in files:
884 files = [b'']
885 # constructing the foldmap is expensive, so don't do it for the
886 # common case where files is ['']
887 normalize = None
888 results = dict.fromkeys(subrepos)
889 results[b'.hg'] = None
890
891 for ff in files:
892 if normalize:
893 nf = normalize(ff, False, True)
894 else:
895 nf = ff
896 if nf in results:
897 continue
898
899 try:
900 st = lstat(join(nf))
901 kind = getkind(st.st_mode)
902 if kind == dirkind:
903 if nf in dmap:
904 # file replaced by dir on disk but still in dirstate
905 results[nf] = None
906 foundadd((nf, ff))
907 elif kind == regkind or kind == lnkkind:
908 results[nf] = st
909 else:
910 badfn(ff, badtype(kind))
911 if nf in dmap:
912 results[nf] = None
913 except OSError as inst: # nf not found on disk - it is dirstate only
914 if nf in dmap: # does it exactly match a missing file?
915 results[nf] = None
916 else: # does it match a missing directory?
917 if self._map.hasdir(nf):
918 notfoundadd(nf)
919 else:
920 badfn(ff, encoding.strtolocal(inst.strerror))
921
922 # match.files() may contain explicitly-specified paths that shouldn't
923 # be taken; drop them from the list of files found. dirsfound/notfound
924 # aren't filtered here because they will be tested later.
925 if match.anypats():
926 for f in list(results):
927 if f == b'.hg' or f in subrepos:
928 # keep sentinel to disable further out-of-repo walks
929 continue
930 if not match(f):
931 del results[f]
932
933 # Case insensitive filesystems cannot rely on lstat() failing to detect
934 # a case-only rename. Prune the stat object for any file that does not
935 # match the case in the filesystem, if there are multiple files that
936 # normalize to the same path.
937 if match.isexact() and self._checkcase:
938 normed = {}
939
940 for f, st in pycompat.iteritems(results):
941 if st is None:
942 continue
943
944 nc = util.normcase(f)
945 paths = normed.get(nc)
946
947 if paths is None:
948 paths = set()
949 normed[nc] = paths
950
951 paths.add(f)
952
953 for norm, paths in pycompat.iteritems(normed):
954 if len(paths) > 1:
955 for path in paths:
956 folded = self._discoverpath(
957 path, norm, True, None, self._map.dirfoldmap
958 )
959 if path != folded:
960 results[path] = None
961
962 return results, dirsfound, dirsnotfound
963
964 def walk(self, match, subrepos, unknown, ignored, full=True):
965 """
966 Walk recursively through the directory tree, finding all files
967 matched by match.
968
969 If full is False, maybe skip some known-clean files.
970
971 Return a dict mapping filename to stat-like object (either
972 mercurial.osutil.stat instance or return value of os.stat()).
973
974 """
975 # full is a flag that extensions that hook into walk can use -- this
976 # implementation doesn't use it at all. This satisfies the contract
977 # because we only guarantee a "maybe".
978
979 if ignored:
980 ignore = util.never
981 dirignore = util.never
982 elif unknown:
983 ignore = self._ignore
984 dirignore = self._dirignore
985 else:
986 # if not unknown and not ignored, drop dir recursion and step 2
987 ignore = util.always
988 dirignore = util.always
989
990 matchfn = match.matchfn
991 matchalways = match.always()
992 matchtdir = match.traversedir
993 dmap = self._map
994 listdir = util.listdir
995 lstat = os.lstat
996 dirkind = stat.S_IFDIR
997 regkind = stat.S_IFREG
998 lnkkind = stat.S_IFLNK
999 join = self._join
1000
1001 exact = skipstep3 = False
1002 if match.isexact(): # match.exact
1003 exact = True
1004 dirignore = util.always # skip step 2
1005 elif match.prefix(): # match.match, no patterns
1006 skipstep3 = True
1007
1008 if not exact and self._checkcase:
1009 normalize = self._normalize
1010 normalizefile = self._normalizefile
1011 skipstep3 = False
1012 else:
1013 normalize = self._normalize
1014 normalizefile = None
1015
1016 # step 1: find all explicit files
1017 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1018 if matchtdir:
1019 for d in work:
1020 matchtdir(d[0])
1021 for d in dirsnotfound:
1022 matchtdir(d)
1023
1024 skipstep3 = skipstep3 and not (work or dirsnotfound)
1025 work = [d for d in work if not dirignore(d[0])]
1026
1027 # step 2: visit subdirectories
1028 def traverse(work, alreadynormed):
1029 wadd = work.append
1030 while work:
1031 tracing.counter('dirstate.walk work', len(work))
1032 nd = work.pop()
1033 visitentries = match.visitchildrenset(nd)
1034 if not visitentries:
1035 continue
1036 if visitentries == b'this' or visitentries == b'all':
1037 visitentries = None
1038 skip = None
1039 if nd != b'':
1040 skip = b'.hg'
1041 try:
1042 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1043 entries = listdir(join(nd), stat=True, skip=skip)
1044 except OSError as inst:
1045 if inst.errno in (errno.EACCES, errno.ENOENT):
1046 match.bad(
1047 self.pathto(nd), encoding.strtolocal(inst.strerror)
1048 )
1049 continue
1050 raise
1051 for f, kind, st in entries:
1052 # Some matchers may return files in the visitentries set,
1053 # instead of 'this', if the matcher explicitly mentions them
1054 # and is not an exactmatcher. This is acceptable; we do not
1055 # make any hard assumptions about file-or-directory below
1056 # based on the presence of `f` in visitentries. If
1057 # visitchildrenset returned a set, we can always skip the
1058 # entries *not* in the set it provided regardless of whether
1059 # they're actually a file or a directory.
1060 if visitentries and f not in visitentries:
1061 continue
1062 if normalizefile:
1063 # even though f might be a directory, we're only
1064 # interested in comparing it to files currently in the
1065 # dmap -- therefore normalizefile is enough
1066 nf = normalizefile(
1067 nd and (nd + b"/" + f) or f, True, True
1068 )
1069 else:
1070 nf = nd and (nd + b"/" + f) or f
1071 if nf not in results:
1072 if kind == dirkind:
1073 if not ignore(nf):
1074 if matchtdir:
1075 matchtdir(nf)
1076 wadd(nf)
1077 if nf in dmap and (matchalways or matchfn(nf)):
1078 results[nf] = None
1079 elif kind == regkind or kind == lnkkind:
1080 if nf in dmap:
1081 if matchalways or matchfn(nf):
1082 results[nf] = st
1083 elif (matchalways or matchfn(nf)) and not ignore(
1084 nf
1085 ):
1086 # unknown file -- normalize if necessary
1087 if not alreadynormed:
1088 nf = normalize(nf, False, True)
1089 results[nf] = st
1090 elif nf in dmap and (matchalways or matchfn(nf)):
1091 results[nf] = None
1092
1093 for nd, d in work:
1094 # alreadynormed means that processwork doesn't have to do any
1095 # expensive directory normalization
1096 alreadynormed = not normalize or nd == d
1097 traverse([d], alreadynormed)
1098
1099 for s in subrepos:
1100 del results[s]
1101 del results[b'.hg']
1102
1103 # step 3: visit remaining files from dmap
1104 if not skipstep3 and not exact:
1105 # If a dmap file is not in results yet, it was either
1106 # a) not matching matchfn b) ignored, c) missing, or d) under a
1107 # symlink directory.
1108 if not results and matchalways:
1109 visit = [f for f in dmap]
1110 else:
1111 visit = [f for f in dmap if f not in results and matchfn(f)]
1112 visit.sort()
1113
1114 if unknown:
1115 # unknown == True means we walked all dirs under the roots
1116 # that wasn't ignored, and everything that matched was stat'ed
1117 # and is already in results.
1118 # The rest must thus be ignored or under a symlink.
1119 audit_path = pathutil.pathauditor(self._root, cached=True)
1120
1121 for nf in iter(visit):
1122 # If a stat for the same file was already added with a
1123 # different case, don't add one for this, since that would
1124 # make it appear as if the file exists under both names
1125 # on disk.
1126 if (
1127 normalizefile
1128 and normalizefile(nf, True, True) in results
1129 ):
1130 results[nf] = None
1131 # Report ignored items in the dmap as long as they are not
1132 # under a symlink directory.
1133 elif audit_path.check(nf):
1134 try:
1135 results[nf] = lstat(join(nf))
1136 # file was just ignored, no links, and exists
1137 except OSError:
1138 # file doesn't exist
1139 results[nf] = None
1140 else:
1141 # It's either missing or under a symlink directory
1142 # which we in this case report as missing
1143 results[nf] = None
1144 else:
1145 # We may not have walked the full directory tree above,
1146 # so stat and check everything we missed.
1147 iv = iter(visit)
1148 for st in util.statfiles([join(i) for i in visit]):
1149 results[next(iv)] = st
1150 return results
1151
1152 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1153 # Force Rayon (Rust parallelism library) to respect the number of
1154 # workers. This is a temporary workaround until Rust code knows
1155 # how to read the config file.
1156 numcpus = self._ui.configint(b"worker", b"numcpus")
1157 if numcpus is not None:
1158 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1159
1160 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1161 if not workers_enabled:
1162 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1163
1164 (
1165 lookup,
1166 modified,
1167 added,
1168 removed,
1169 deleted,
1170 clean,
1171 ignored,
1172 unknown,
1173 warnings,
1174 bad,
1175 traversed,
1176 dirty,
1177 ) = rustmod.status(
1178 self._map._rustmap,
1179 matcher,
1180 self._rootdir,
1181 self._ignorefiles(),
1182 self._checkexec,
1183 self._lastnormaltime,
1184 bool(list_clean),
1185 bool(list_ignored),
1186 bool(list_unknown),
1187 bool(matcher.traversedir),
1188 )
1189
1190 self._dirty |= dirty
1191
1192 if matcher.traversedir:
1193 for dir in traversed:
1194 matcher.traversedir(dir)
1195
1196 if self._ui.warn:
1197 for item in warnings:
1198 if isinstance(item, tuple):
1199 file_path, syntax = item
1200 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1201 file_path,
1202 syntax,
1203 )
1204 self._ui.warn(msg)
1205 else:
1206 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1207 self._ui.warn(
1208 msg
1209 % (
1210 pathutil.canonpath(
1211 self._rootdir, self._rootdir, item
1212 ),
1213 b"No such file or directory",
1214 )
1215 )
1216
1217 for (fn, message) in bad:
1218 matcher.bad(fn, encoding.strtolocal(message))
1219
1220 status = scmutil.status(
1221 modified=modified,
1222 added=added,
1223 removed=removed,
1224 deleted=deleted,
1225 unknown=unknown,
1226 ignored=ignored,
1227 clean=clean,
1228 )
1229 return (lookup, status)
1230
1231 def status(self, match, subrepos, ignored, clean, unknown):
1232 """Determine the status of the working copy relative to the
1233 dirstate and return a pair of (unsure, status), where status is of type
1234 scmutil.status and:
1235
1236 unsure:
1237 files that might have been modified since the dirstate was
1238 written, but need to be read to be sure (size is the same
1239 but mtime differs)
1240 status.modified:
1241 files that have definitely been modified since the dirstate
1242 was written (different size or mode)
1243 status.clean:
1244 files that have definitely not been modified since the
1245 dirstate was written
1246 """
1247 listignored, listclean, listunknown = ignored, clean, unknown
1248 lookup, modified, added, unknown, ignored = [], [], [], [], []
1249 removed, deleted, clean = [], [], []
1250
1251 dmap = self._map
1252 dmap.preload()
1253
1254 use_rust = True
1255
1256 allowed_matchers = (
1257 matchmod.alwaysmatcher,
1258 matchmod.exactmatcher,
1259 matchmod.includematcher,
1260 )
1261
1262 if rustmod is None:
1263 use_rust = False
1264 elif self._checkcase:
1265 # Case-insensitive filesystems are not handled yet
1266 use_rust = False
1267 elif subrepos:
1268 use_rust = False
1269 elif sparse.enabled:
1270 use_rust = False
1271 elif not isinstance(match, allowed_matchers):
1272 # Some matchers have yet to be implemented
1273 use_rust = False
1274
1275 if use_rust:
1276 try:
1277 return self._rust_status(
1278 match, listclean, listignored, listunknown
1279 )
1280 except rustmod.FallbackError:
1281 pass
1282
1283 def noop(f):
1284 pass
1285
1286 dcontains = dmap.__contains__
1287 dget = dmap.__getitem__
1288 ladd = lookup.append # aka "unsure"
1289 madd = modified.append
1290 aadd = added.append
1291 uadd = unknown.append if listunknown else noop
1292 iadd = ignored.append if listignored else noop
1293 radd = removed.append
1294 dadd = deleted.append
1295 cadd = clean.append if listclean else noop
1296 mexact = match.exact
1297 dirignore = self._dirignore
1298 checkexec = self._checkexec
1299 copymap = self._map.copymap
1300 lastnormaltime = self._lastnormaltime
1301
1302 # We need to do full walks when either
1303 # - we're listing all clean files, or
1304 # - match.traversedir does something, because match.traversedir should
1305 # be called for every dir in the working dir
1306 full = listclean or match.traversedir is not None
1307 for fn, st in pycompat.iteritems(
1308 self.walk(match, subrepos, listunknown, listignored, full=full)
1309 ):
1310 if not dcontains(fn):
1311 if (listignored or mexact(fn)) and dirignore(fn):
1312 if listignored:
1313 iadd(fn)
1314 else:
1315 uadd(fn)
1316 continue
1317
1318 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1319 # written like that for performance reasons. dmap[fn] is not a
1320 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1321 # opcode has fast paths when the value to be unpacked is a tuple or
1322 # a list, but falls back to creating a full-fledged iterator in
1323 # general. That is much slower than simply accessing and storing the
1324 # tuple members one by one.
1325 t = dget(fn)
1326 state = t[0]
1327 mode = t[1]
1328 size = t[2]
1329 time = t[3]
1330
1331 if not st and state in b"nma":
1332 dadd(fn)
1333 elif state == b'n':
1334 if (
1335 size >= 0
1336 and (
1337 (size != st.st_size and size != st.st_size & _rangemask)
1338 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1339 )
1340 or size == FROM_P2 # other parent
1341 or fn in copymap
1342 ):
1343 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1344 # issue6456: Size returned may be longer due to
1345 # encryption on EXT-4 fscrypt, undecided.
1346 ladd(fn)
1347 else:
1348 madd(fn)
1349 elif (
1350 time != st[stat.ST_MTIME]
1351 and time != st[stat.ST_MTIME] & _rangemask
1352 ):
1353 ladd(fn)
1354 elif st[stat.ST_MTIME] == lastnormaltime:
1355 # fn may have just been marked as normal and it may have
1356 # changed in the same second without changing its size.
1357 # This can happen if we quickly do multiple commits.
1358 # Force lookup, so we don't miss such a racy file change.
1359 ladd(fn)
1360 elif listclean:
1361 cadd(fn)
1362 elif state == b'm':
1363 madd(fn)
1364 elif state == b'a':
1365 aadd(fn)
1366 elif state == b'r':
1367 radd(fn)
1368 status = scmutil.status(
1369 modified, added, removed, deleted, unknown, ignored, clean
1370 )
1371 return (lookup, status)
1372
1373 def matches(self, match):
1374 """
1375 return files in the dirstate (in whatever state) filtered by match
1376 """
1377 dmap = self._map
1378 if rustmod is not None:
1379 dmap = self._map._rustmap
1380
1381 if match.always():
1382 return dmap.keys()
1383 files = match.files()
1384 if match.isexact():
1385 # fast path -- filter the other way around, since typically files is
1386 # much smaller than dmap
1387 return [f for f in files if f in dmap]
1388 if match.prefix() and all(fn in dmap for fn in files):
1389 # fast path -- all the values are known to be files, so just return
1390 # that
1391 return list(files)
1392 return [f for f in dmap if match(f)]
1393
1394 def _actualfilename(self, tr):
1395 if tr:
1396 return self._pendingfilename
1397 else:
1398 return self._filename
1399
1400 def savebackup(self, tr, backupname):
1401 '''Save current dirstate into backup file'''
1402 filename = self._actualfilename(tr)
1403 assert backupname != filename
1404
1405 # use '_writedirstate' instead of 'write' to write changes certainly,
1406 # because the latter omits writing out if transaction is running.
1407 # output file will be used to create backup of dirstate at this point.
1408 if self._dirty or not self._opener.exists(filename):
1409 self._writedirstate(
1410 self._opener(filename, b"w", atomictemp=True, checkambig=True)
1411 )
1412
1413 if tr:
1414 # ensure that subsequent tr.writepending returns True for
1415 # changes written out above, even if dirstate is never
1416 # changed after this
1417 tr.addfilegenerator(
1418 b'dirstate',
1419 (self._filename,),
1420 self._writedirstate,
1421 location=b'plain',
1422 )
1423
1424 # ensure that pending file written above is unlinked at
1425 # failure, even if tr.writepending isn't invoked until the
1426 # end of this transaction
1427 tr.registertmp(filename, location=b'plain')
1428
1429 self._opener.tryunlink(backupname)
1430 # hardlink backup is okay because _writedirstate is always called
1431 # with an "atomictemp=True" file.
1432 util.copyfile(
1433 self._opener.join(filename),
1434 self._opener.join(backupname),
1435 hardlink=True,
1436 )
1437
1438 def restorebackup(self, tr, backupname):
1439 '''Restore dirstate by backup file'''
1440 # this "invalidate()" prevents "wlock.release()" from writing
1441 # changes of dirstate out after restoring from backup file
1442 self.invalidate()
1443 filename = self._actualfilename(tr)
1444 o = self._opener
1445 if util.samefile(o.join(backupname), o.join(filename)):
1446 o.unlink(backupname)
1447 else:
1448 o.rename(backupname, filename, checkambig=True)
1449
1450 def clearbackup(self, tr, backupname):
1451 '''Clear backup file'''
1452 self._opener.unlink(backupname)
1453
1454
1455 class dirstatemap(object):
39 class dirstatemap(object):
1456 """Map encapsulating the dirstate's contents.
40 """Map encapsulating the dirstate's contents.
1457
41
@@ -10,6 +10,7 b' from __future__ import absolute_import'
10 from mercurial import (
10 from mercurial import (
11 context,
11 context,
12 dirstate,
12 dirstate,
13 dirstatemap as dirstatemapmod,
13 extensions,
14 extensions,
14 policy,
15 policy,
15 registrar,
16 registrar,
@@ -66,11 +67,11 b' def fakewrite(ui, func):'
66 if rustmod is not None:
67 if rustmod is not None:
67 # The Rust implementation does not use public parse/pack dirstate
68 # The Rust implementation does not use public parse/pack dirstate
68 # to prevent conversion round-trips
69 # to prevent conversion round-trips
69 orig_dirstatemap_write = dirstate.dirstatemap.write
70 orig_dirstatemap_write = dirstatemapmod.dirstatemap.write
70 wrapper = lambda self, st, now: orig_dirstatemap_write(
71 wrapper = lambda self, st, now: orig_dirstatemap_write(
71 self, st, fakenow
72 self, st, fakenow
72 )
73 )
73 dirstate.dirstatemap.write = wrapper
74 dirstatemapmod.dirstatemap.write = wrapper
74
75
75 orig_dirstate_getfsnow = dirstate._getfsnow
76 orig_dirstate_getfsnow = dirstate._getfsnow
76 wrapper = lambda *args: pack_dirstate(fakenow, orig_pack_dirstate, *args)
77 wrapper = lambda *args: pack_dirstate(fakenow, orig_pack_dirstate, *args)
@@ -86,7 +87,7 b' def fakewrite(ui, func):'
86 orig_module.pack_dirstate = orig_pack_dirstate
87 orig_module.pack_dirstate = orig_pack_dirstate
87 dirstate._getfsnow = orig_dirstate_getfsnow
88 dirstate._getfsnow = orig_dirstate_getfsnow
88 if rustmod is not None:
89 if rustmod is not None:
89 dirstate.dirstatemap.write = orig_dirstatemap_write
90 dirstatemapmod.dirstatemap.write = orig_dirstatemap_write
90
91
91
92
92 def _poststatusfixup(orig, workingctx, status, fixup):
93 def _poststatusfixup(orig, workingctx, status, fixup):
General Comments 0
You need to be logged in to leave comments. Login now