/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/osutils.py

  • Committer: Gustav Hartvigsson
  • Date: 2021-01-09 21:36:27 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20210109213627-h1xwcutzy9m7a99b
Added 'Case Preserving Working Tree Use Cases' from Canonical Wiki

* Addod a page from the Canonical Bazaar wiki
  with information on the scmeatics of case
  perserving filesystems an a case insensitive
  filesystem works.
  
  * Needs re-work, but this will do as it is the
    same inforamoton as what was on the linked
    page in the currint documentation.

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
 
from __future__ import absolute_import
18
 
 
19
17
import errno
20
18
import os
21
19
import re
52
50
from breezy.i18n import gettext
53
51
""")
54
52
 
55
 
from .sixish import (
56
 
    PY3,
57
 
    text_type,
58
 
    )
59
 
 
60
53
from hashlib import (
61
54
    md5,
62
55
    sha1 as sha,
70
63
    )
71
64
 
72
65
 
73
 
# Cross platform wall-clock time functionality with decent resolution.
74
 
# On Linux ``time.clock`` returns only CPU time. On Windows, ``time.time()``
75
 
# only has a resolution of ~15ms. Note that ``time.clock()`` is not
76
 
# synchronized with ``time.time()``, this is only meant to be used to find
77
 
# delta times by subtracting from another call to this function.
78
 
timer_func = time.time
79
 
if sys.platform == 'win32':
80
 
    timer_func = time.clock
81
 
 
82
66
# On win32, O_BINARY is used to indicate the file should
83
67
# be opened in binary mode, rather than text mode.
84
68
# On other platforms, O_BINARY doesn't exist, because
99
83
        self.timezone = timezone
100
84
 
101
85
 
102
 
def get_unicode_argv():
103
 
    if PY3:
104
 
        return sys.argv[1:]
105
 
    try:
106
 
        user_encoding = get_user_encoding()
107
 
        return [a.decode(user_encoding) for a in sys.argv[1:]]
108
 
    except UnicodeDecodeError:
109
 
        raise errors.BzrError(gettext("Parameter {0!r} encoding is unsupported by {1} "
110
 
                                      "application locale.").format(a, user_encoding))
111
 
 
112
 
 
113
86
def make_readonly(filename):
114
87
    """Make a filename read-only."""
115
88
    mod = os.lstat(filename).st_mode
334
307
    return path
335
308
 
336
309
 
337
 
def _posix_path_from_environ(key):
338
 
    """Get unicode path from `key` in environment or None if not present
339
 
 
340
 
    Note that posix systems use arbitrary byte strings for filesystem objects,
341
 
    so a path that raises BadFilenameEncoding here may still be accessible.
342
 
    """
343
 
    val = os.environ.get(key, None)
344
 
    if PY3 or val is None:
345
 
        return val
346
 
    try:
347
 
        return val.decode(_fs_enc)
348
 
    except UnicodeDecodeError:
349
 
        # GZ 2011-12-12:Ideally want to include `key` in the exception message
350
 
        raise errors.BadFilenameEncoding(val, _fs_enc)
351
 
 
352
 
 
353
310
def _posix_get_home_dir():
354
311
    """Get the home directory of the current user as a unicode path"""
355
312
    path = posixpath.expanduser("~")
363
320
 
364
321
def _posix_getuser_unicode():
365
322
    """Get username from environment or password database as unicode"""
366
 
    name = getpass.getuser()
367
 
    if PY3:
368
 
        return name
369
 
    user_encoding = get_user_encoding()
370
 
    try:
371
 
        return name.decode(user_encoding)
372
 
    except UnicodeDecodeError:
373
 
        raise errors.BzrError("Encoding of username %r is unsupported by %s "
374
 
                              "application locale." % (name, user_encoding))
 
323
    return getpass.getuser()
375
324
 
376
325
 
377
326
def _win32_fixdrive(path):
456
405
    return _rename_wrapper
457
406
 
458
407
 
459
 
if sys.version_info > (3,):
460
 
    _getcwd = os.getcwd
461
 
else:
462
 
    _getcwd = os.getcwdu
 
408
_getcwd = os.getcwd
463
409
 
464
410
 
465
411
# Default rename wraps os.rename()
471
417
realpath = _posix_realpath
472
418
pathjoin = os.path.join
473
419
normpath = _posix_normpath
474
 
path_from_environ = _posix_path_from_environ
475
420
_get_home_dir = _posix_get_home_dir
476
421
getuser_unicode = _posix_getuser_unicode
477
422
getcwd = _getcwd
529
474
        """Replacer for shutil.rmtree: could remove readonly dirs/files"""
530
475
        return shutil.rmtree(path, ignore_errors, onerror)
531
476
 
532
 
    f = win32utils.get_unicode_argv     # special function or None
533
 
    if f is not None:
534
 
        get_unicode_argv = f
535
 
    path_from_environ = win32utils.get_environ_unicode
536
477
    _get_home_dir = win32utils.get_home_location
537
478
    getuser_unicode = win32utils.get_user_name
538
479
 
749
690
 
750
691
# GZ 2017-09-16: Makes sense in general for hexdigest() result to be text, but
751
692
# used as bytes through most interfaces so encode with this wrapper.
752
 
if PY3:
753
 
    def _hexdigest(hashobj):
754
 
        return hashobj.hexdigest().encode()
755
 
else:
756
 
    def _hexdigest(hashobj):
757
 
        return hashobj.hexdigest()
 
693
def _hexdigest(hashobj):
 
694
    return hashobj.hexdigest().encode()
758
695
 
759
696
 
760
697
def sha_file(f):
906
843
    (date_fmt, tt, offset_str) = \
907
844
        _format_date(t, offset, timezone, date_fmt, show_offset)
908
845
    date_str = time.strftime(date_fmt, tt)
909
 
    if not isinstance(date_str, text_type):
 
846
    if not isinstance(date_str, str):
910
847
        date_str = date_str.decode(get_user_encoding(), 'replace')
911
848
    return date_str + offset_str
912
849
 
1023
960
    """
1024
961
    s = ''
1025
962
    for raw_byte in rand_bytes(num):
1026
 
        if not PY3:
1027
 
            s += ALNUM[ord(raw_byte) % 36]
1028
 
        else:
1029
 
            s += ALNUM[raw_byte % 36]
 
963
        s += ALNUM[raw_byte % 36]
1030
964
    return s
1031
965
 
1032
966
 
1035
969
 
1036
970
def splitpath(p):
1037
971
    """Turn string into list of parts."""
 
972
    use_bytes = isinstance(p, bytes)
1038
973
    if os.path.sep == '\\':
1039
974
        # split on either delimiter because people might use either on
1040
975
        # Windows
1041
 
        if isinstance(p, bytes):
 
976
        if use_bytes:
1042
977
            ps = re.split(b'[\\\\/]', p)
1043
978
        else:
1044
979
            ps = re.split(r'[\\/]', p)
1045
980
    else:
1046
 
        if isinstance(p, bytes):
 
981
        if use_bytes:
1047
982
            ps = p.split(b'/')
1048
983
        else:
1049
984
            ps = p.split('/')
1050
985
 
 
986
    if use_bytes:
 
987
        parent_dir = b'..'
 
988
        current_empty_dir = (b'.', b'')
 
989
    else:
 
990
        parent_dir = '..'
 
991
        current_empty_dir = ('.', '')
 
992
 
1051
993
    rps = []
1052
994
    for f in ps:
1053
 
        if f in ('..', b'..'):
 
995
        if f == parent_dir:
1054
996
            raise errors.BzrError(gettext("sorry, %r not allowed in path") % f)
1055
 
        elif f in ('.', '', b'.', b''):
 
997
        elif f in current_empty_dir:
1056
998
            pass
1057
999
        else:
1058
1000
            rps.append(f)
1325
1267
 
1326
1268
    abs_base = abspath(base)
1327
1269
    current = abs_base
1328
 
    _listdir = os.listdir
1329
1270
 
1330
1271
    # use an explicit iterator so we can easily consume the rest on early exit.
1331
1272
    bit_iter = iter(rel.split('/'))
1332
1273
    for bit in bit_iter:
1333
1274
        lbit = bit.lower()
1334
1275
        try:
1335
 
            next_entries = _listdir(current)
 
1276
            next_entries = scandir(current)
1336
1277
        except OSError:  # enoent, eperm, etc
1337
1278
            # We can't find this in the filesystem, so just append the
1338
1279
            # remaining bits.
1339
1280
            current = pathjoin(current, bit, *list(bit_iter))
1340
1281
            break
1341
 
        for look in next_entries:
1342
 
            if lbit == look.lower():
1343
 
                current = pathjoin(current, look)
 
1282
        for entry in next_entries:
 
1283
            if lbit == entry.name.lower():
 
1284
                current = entry.path
1344
1285
                break
1345
1286
        else:
1346
1287
            # got to the end, nothing matched, so we just return the
1379
1320
    Otherwise it is decoded from the the filesystem's encoding. If decoding
1380
1321
    fails, a errors.BadFilenameEncoding exception is raised.
1381
1322
    """
1382
 
    if isinstance(filename, text_type):
 
1323
    if isinstance(filename, str):
1383
1324
        return filename
1384
1325
    try:
1385
1326
        return filename.decode(_fs_enc)
1394
1335
    Otherwise it is decoded from utf-8. If decoding fails, the exception is
1395
1336
    wrapped in a BzrBadParameterNotUnicode exception.
1396
1337
    """
1397
 
    if isinstance(unicode_or_utf8_string, text_type):
 
1338
    if isinstance(unicode_or_utf8_string, str):
1398
1339
        return unicode_or_utf8_string
1399
1340
    try:
1400
1341
        return unicode_or_utf8_string.decode('utf8')
1421
1362
    return unicode_or_utf8_string.encode('utf-8')
1422
1363
 
1423
1364
 
1424
 
def safe_revision_id(unicode_or_utf8_string):
1425
 
    """Revision ids should now be utf8, but at one point they were unicode.
1426
 
 
1427
 
    :param unicode_or_utf8_string: A possibly Unicode revision_id. (can also be
1428
 
        utf8 or None).
1429
 
    :return: None or a utf8 revision id.
1430
 
    """
1431
 
    if (unicode_or_utf8_string is None
1432
 
            or unicode_or_utf8_string.__class__ == bytes):
1433
 
        return unicode_or_utf8_string
1434
 
    raise TypeError('Unicode revision ids are no longer supported. '
1435
 
                    'Revision id generators should be creating utf8 revision '
1436
 
                    'ids.')
1437
 
 
1438
 
 
1439
 
def safe_file_id(unicode_or_utf8_string):
1440
 
    """File ids should now be utf8, but at one point they were unicode.
1441
 
 
1442
 
    This is the same as safe_utf8, except it uses the cached encode functions
1443
 
    to save a little bit of performance.
1444
 
 
1445
 
    :param unicode_or_utf8_string: A possibly Unicode file_id. (can also be
1446
 
        utf8 or None).
1447
 
    :return: None or a utf8 file id.
1448
 
    """
1449
 
    if (unicode_or_utf8_string is None
1450
 
            or unicode_or_utf8_string.__class__ == bytes):
1451
 
        return unicode_or_utf8_string
1452
 
    raise TypeError('Unicode file ids are no longer supported. '
1453
 
                    'File id generators should be creating utf8 file ids.')
1454
 
 
1455
 
 
1456
1365
_platform_normalizes_filenames = False
1457
1366
if sys.platform == 'darwin':
1458
1367
    _platform_normalizes_filenames = True
1671
1580
    _terminal_size = _ioctl_terminal_size
1672
1581
 
1673
1582
 
1674
 
def supports_executable():
1675
 
    return sys.platform != "win32"
 
1583
def supports_executable(path):
 
1584
    """Return if filesystem at path supports executable bit.
 
1585
 
 
1586
    :param path: Path for which to check the file system
 
1587
    :return: boolean indicating whether executable bit can be stored/relied upon
 
1588
    """
 
1589
    if sys.platform == 'win32':
 
1590
        return False
 
1591
    try:
 
1592
        fs_type = get_fs_type(path)
 
1593
    except errors.DependencyNotPresent as e:
 
1594
        trace.mutter('Unable to get fs type for %r: %s', path, e)
 
1595
    else:
 
1596
        if fs_type in ('vfat', 'ntfs'):
 
1597
            # filesystems known to not support executable bit
 
1598
            return False
 
1599
    return True
 
1600
 
 
1601
 
 
1602
def supports_symlinks(path):
 
1603
    """Return if the filesystem at path supports the creation of symbolic links.
 
1604
 
 
1605
    """
 
1606
    if not has_symlinks():
 
1607
        return False
 
1608
    try:
 
1609
        fs_type = get_fs_type(path)
 
1610
    except errors.DependencyNotPresent as e:
 
1611
        trace.mutter('Unable to get fs type for %r: %s', path, e)
 
1612
    else:
 
1613
        if fs_type in ('vfat', 'ntfs'):
 
1614
            # filesystems known to not support symlinks
 
1615
            return False
 
1616
    return True
1676
1617
 
1677
1618
 
1678
1619
def supports_posix_readonly():
1701
1642
        if orig_val is not None:
1702
1643
            del os.environ[env_variable]
1703
1644
    else:
1704
 
        if not PY3 and isinstance(value, text_type):
1705
 
            value = value.encode(get_user_encoding())
1706
1645
        os.environ[env_variable] = value
1707
1646
    return orig_val
1708
1647
 
1724
1663
_WIN32_ERROR_DIRECTORY = 267  # Similar to errno.ENOTDIR
1725
1664
 
1726
1665
 
 
1666
try:
 
1667
    scandir = os.scandir
 
1668
except AttributeError:  # Python < 3
 
1669
    lazy_import(globals(), """\
 
1670
from scandir import scandir
 
1671
""")
 
1672
 
 
1673
 
1727
1674
def _is_error_enotdir(e):
1728
1675
    """Check if this exception represents ENOTDIR.
1729
1676
 
1783
1730
    # depending on top and prefix - i.e. ./foo and foo as a pair leads to
1784
1731
    # potentially confusing output. We should make this more robust - but
1785
1732
    # not at a speed cost. RBC 20060731
1786
 
    _lstat = os.lstat
1787
1733
    _directory = _directory_kind
1788
 
    _listdir = os.listdir
1789
 
    _kind_from_mode = file_kind_from_stat_mode
1790
1734
    pending = [(safe_unicode(prefix), "", _directory, None, safe_unicode(top))]
1791
1735
    while pending:
1792
1736
        # 0 - relpath, 1- basename, 2- kind, 3- stat, 4-toppath
1798
1742
        top_slash = top + u'/'
1799
1743
 
1800
1744
        dirblock = []
1801
 
        append = dirblock.append
1802
1745
        try:
1803
 
            names = sorted(map(decode_filename, _listdir(top)))
 
1746
            for entry in scandir(top):
 
1747
                name = decode_filename(entry.name)
 
1748
                statvalue = entry.stat(follow_symlinks=False)
 
1749
                kind = file_kind_from_stat_mode(statvalue.st_mode)
 
1750
                dirblock.append((relprefix + name, name, kind, statvalue, entry.path))
1804
1751
        except OSError as e:
1805
1752
            if not _is_error_enotdir(e):
1806
1753
                raise
1807
 
        else:
1808
 
            for name in names:
1809
 
                abspath = top_slash + name
1810
 
                statvalue = _lstat(abspath)
1811
 
                kind = _kind_from_mode(statvalue.st_mode)
1812
 
                append((relprefix + name, name, kind, statvalue, abspath))
 
1754
        except UnicodeDecodeError as e:
 
1755
            raise errors.BadFilenameEncoding(e.object, _fs_enc)
 
1756
        dirblock.sort()
1813
1757
        yield (relroot, top), dirblock
1814
1758
 
1815
1759
        # push the user specified dirs from dirblock
1925
1869
        def _fs_decode(s): return s.decode(_fs_enc)
1926
1870
 
1927
1871
        def _fs_encode(s): return s.encode(_fs_enc)
1928
 
        _lstat = os.lstat
1929
 
        _listdir = os.listdir
1930
 
        _kind_from_mode = file_kind_from_stat_mode
1931
1872
 
1932
1873
        if prefix:
1933
1874
            relprefix = prefix + b'/'
1937
1878
 
1938
1879
        dirblock = []
1939
1880
        append = dirblock.append
1940
 
        for name_native in _listdir(top.encode('utf-8')):
 
1881
        for entry in scandir(safe_utf8(top)):
1941
1882
            try:
1942
 
                name = _fs_decode(name_native)
 
1883
                name = _fs_decode(entry.name)
1943
1884
            except UnicodeDecodeError:
1944
1885
                raise errors.BadFilenameEncoding(
1945
 
                    relprefix + name_native, _fs_enc)
 
1886
                    relprefix + entry.name, _fs_enc)
 
1887
            abspath = top_slash + name
1946
1888
            name_utf8 = _utf8_encode(name)[0]
1947
 
            abspath = top_slash + name
1948
 
            statvalue = _lstat(abspath)
1949
 
            kind = _kind_from_mode(statvalue.st_mode)
 
1889
            statvalue = entry.stat(follow_symlinks=False)
 
1890
            kind = file_kind_from_stat_mode(statvalue.st_mode)
1950
1891
            append((relprefix + name_utf8, name_utf8, kind, statvalue, abspath))
1951
1892
        return sorted(dirblock)
1952
1893
 
2097
2038
        return win32utils.get_host_name()
2098
2039
    else:
2099
2040
        import socket
2100
 
        if PY3:
2101
 
            return socket.gethostname()
2102
 
        return socket.gethostname().decode(get_user_encoding())
 
2041
        return socket.gethostname()
2103
2042
 
2104
2043
 
2105
2044
# We must not read/write any more than 64k at a time from/to a socket so we
2598
2537
            raise exception_class(path)
2599
2538
 
2600
2539
 
2601
 
def is_environment_error(evalue):
2602
 
    """True if exception instance is due to a process environment issue
2603
 
 
2604
 
    This includes OSError and IOError, but also other errors that come from
2605
 
    the operating system or core libraries but are not subclasses of those.
2606
 
    """
2607
 
    if isinstance(evalue, (EnvironmentError, select.error)):
2608
 
        return True
2609
 
    if sys.platform == "win32" and win32utils._is_pywintypes_error(evalue):
2610
 
        return True
2611
 
    return False
 
2540
def read_mtab(path):
 
2541
    """Read an fstab-style file and extract mountpoint+filesystem information.
 
2542
 
 
2543
    :param path: Path to read from
 
2544
    :yield: Tuples with mountpoints (as bytestrings) and filesystem names
 
2545
    """
 
2546
    with open(path, 'rb') as f:
 
2547
        for line in f:
 
2548
            if line.startswith(b'#'):
 
2549
                continue
 
2550
            cols = line.split()
 
2551
            if len(cols) < 3:
 
2552
                continue
 
2553
            yield cols[1], cols[2].decode('ascii', 'replace')
 
2554
 
 
2555
 
 
2556
MTAB_PATH = '/etc/mtab'
 
2557
 
 
2558
class FilesystemFinder(object):
 
2559
    """Find the filesystem for a particular path."""
 
2560
 
 
2561
    def __init__(self, mountpoints):
 
2562
        def key(x):
 
2563
            return len(x[0])
 
2564
        self._mountpoints = sorted(mountpoints, key=key, reverse=True)
 
2565
 
 
2566
    @classmethod
 
2567
    def from_mtab(cls):
 
2568
        """Create a FilesystemFinder from an mtab-style file.
 
2569
 
 
2570
        Note that this will silenty ignore mtab if it doesn't exist or can not
 
2571
        be opened.
 
2572
        """
 
2573
        # TODO(jelmer): Use inotify to be notified when /etc/mtab changes and
 
2574
        # we need to re-read it.
 
2575
        try:
 
2576
            return cls(read_mtab(MTAB_PATH))
 
2577
        except EnvironmentError as e:
 
2578
            trace.mutter('Unable to read mtab: %s', e)
 
2579
            return cls([])
 
2580
 
 
2581
    def find(self, path):
 
2582
        """Find the filesystem used by a particular path.
 
2583
 
 
2584
        :param path: Path to find (bytestring or text type)
 
2585
        :return: Filesystem name (as text type) or None, if the filesystem is
 
2586
            unknown.
 
2587
        """
 
2588
        for mountpoint, filesystem in self._mountpoints:
 
2589
            if is_inside(mountpoint, path):
 
2590
                return filesystem
 
2591
        return None
 
2592
 
 
2593
 
 
2594
_FILESYSTEM_FINDER = None
 
2595
 
 
2596
 
 
2597
def get_fs_type(path):
 
2598
    """Return the filesystem type for the partition a path is in.
 
2599
 
 
2600
    :param path: Path to search filesystem type for
 
2601
    :return: A FS type, as string. E.g. "ext2"
 
2602
    """
 
2603
    global _FILESYSTEM_FINDER
 
2604
    if _FILESYSTEM_FINDER is None:
 
2605
        _FILESYSTEM_FINDER = FilesystemFinder.from_mtab()
 
2606
 
 
2607
    if not isinstance(path, bytes):
 
2608
        path = path.encode(_fs_enc)
 
2609
 
 
2610
    return _FILESYSTEM_FINDER.find(path)
 
2611
 
 
2612
 
 
2613
perf_counter = time.perf_counter