##// END OF EJS Templates
typing: add type hints to the rest of the posix module...
Matt Harbison -
r50711:ae93ada0 default
parent child Browse files
Show More
@@ -1,775 +1,777 b''
1 # posix.py - Posix utility function implementations for Mercurial
1 # posix.py - Posix utility function implementations for Mercurial
2 #
2 #
3 # Copyright 2005-2009 Olivia Mackall <olivia@selenic.com> and others
3 # Copyright 2005-2009 Olivia Mackall <olivia@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8
8
9 import errno
9 import errno
10 import fcntl
10 import fcntl
11 import getpass
11 import getpass
12 import grp
12 import grp
13 import os
13 import os
14 import pwd
14 import pwd
15 import re
15 import re
16 import select
16 import select
17 import stat
17 import stat
18 import sys
18 import sys
19 import tempfile
19 import tempfile
20 import unicodedata
20 import unicodedata
21
21
22 from typing import (
22 from typing import (
23 Any,
23 Any,
24 Iterable,
24 Iterable,
25 Iterator,
25 Iterator,
26 List,
26 List,
27 Match,
27 NoReturn,
28 NoReturn,
28 Optional,
29 Optional,
29 Sequence,
30 Sequence,
31 Tuple,
30 Union,
32 Union,
31 )
33 )
32
34
33 from .i18n import _
35 from .i18n import _
34 from .pycompat import (
36 from .pycompat import (
35 getattr,
37 getattr,
36 open,
38 open,
37 )
39 )
38 from . import (
40 from . import (
39 encoding,
41 encoding,
40 error,
42 error,
41 policy,
43 policy,
42 pycompat,
44 pycompat,
43 )
45 )
44
46
45 osutil = policy.importmod('osutil')
47 osutil = policy.importmod('osutil')
46
48
47 normpath = os.path.normpath
49 normpath = os.path.normpath
48 samestat = os.path.samestat
50 samestat = os.path.samestat
49 abspath = os.path.abspath # re-exports
51 abspath = os.path.abspath # re-exports
50
52
51 try:
53 try:
52 oslink = os.link
54 oslink = os.link
53 except AttributeError:
55 except AttributeError:
54 # Some platforms build Python without os.link on systems that are
56 # Some platforms build Python without os.link on systems that are
55 # vaguely unix-like but don't have hardlink support. For those
57 # vaguely unix-like but don't have hardlink support. For those
56 # poor souls, just say we tried and that it failed so we fall back
58 # poor souls, just say we tried and that it failed so we fall back
57 # to copies.
59 # to copies.
58 def oslink(src: bytes, dst: bytes) -> NoReturn:
60 def oslink(src: bytes, dst: bytes) -> NoReturn:
59 raise OSError(
61 raise OSError(
60 errno.EINVAL, b'hardlinks not supported: %s to %s' % (src, dst)
62 errno.EINVAL, b'hardlinks not supported: %s to %s' % (src, dst)
61 )
63 )
62
64
63
65
64 readlink = os.readlink
66 readlink = os.readlink
65 unlink = os.unlink
67 unlink = os.unlink
66 rename = os.rename
68 rename = os.rename
67 removedirs = os.removedirs
69 removedirs = os.removedirs
68 expandglobs = False
70 expandglobs: bool = False
69
71
70 umask = os.umask(0)
72 umask: int = os.umask(0)
71 os.umask(umask)
73 os.umask(umask)
72
74
73 posixfile = open
75 posixfile = open
74
76
75
77
76 def split(p):
78 def split(p: bytes) -> Tuple[bytes, bytes]:
77 """Same as posixpath.split, but faster
79 """Same as posixpath.split, but faster
78
80
79 >>> import posixpath
81 >>> import posixpath
80 >>> for f in [b'/absolute/path/to/file',
82 >>> for f in [b'/absolute/path/to/file',
81 ... b'relative/path/to/file',
83 ... b'relative/path/to/file',
82 ... b'file_alone',
84 ... b'file_alone',
83 ... b'path/to/directory/',
85 ... b'path/to/directory/',
84 ... b'/multiple/path//separators',
86 ... b'/multiple/path//separators',
85 ... b'/file_at_root',
87 ... b'/file_at_root',
86 ... b'///multiple_leading_separators_at_root',
88 ... b'///multiple_leading_separators_at_root',
87 ... b'']:
89 ... b'']:
88 ... assert split(f) == posixpath.split(f), f
90 ... assert split(f) == posixpath.split(f), f
89 """
91 """
90 ht = p.rsplit(b'/', 1)
92 ht = p.rsplit(b'/', 1)
91 if len(ht) == 1:
93 if len(ht) == 1:
92 return b'', p
94 return b'', p
93 nh = ht[0].rstrip(b'/')
95 nh = ht[0].rstrip(b'/')
94 if nh:
96 if nh:
95 return nh, ht[1]
97 return nh, ht[1]
96 return ht[0] + b'/', ht[1]
98 return ht[0] + b'/', ht[1]
97
99
98
100
99 def openhardlinks() -> bool:
101 def openhardlinks() -> bool:
100 '''return true if it is safe to hold open file handles to hardlinks'''
102 '''return true if it is safe to hold open file handles to hardlinks'''
101 return True
103 return True
102
104
103
105
104 def nlinks(name: bytes) -> int:
106 def nlinks(name: bytes) -> int:
105 '''return number of hardlinks for the given file'''
107 '''return number of hardlinks for the given file'''
106 return os.lstat(name).st_nlink
108 return os.lstat(name).st_nlink
107
109
108
110
109 def parsepatchoutput(output_line: bytes) -> bytes:
111 def parsepatchoutput(output_line: bytes) -> bytes:
110 """parses the output produced by patch and returns the filename"""
112 """parses the output produced by patch and returns the filename"""
111 pf = output_line[14:]
113 pf = output_line[14:]
112 if pycompat.sysplatform == b'OpenVMS':
114 if pycompat.sysplatform == b'OpenVMS':
113 if pf[0] == b'`':
115 if pf[0] == b'`':
114 pf = pf[1:-1] # Remove the quotes
116 pf = pf[1:-1] # Remove the quotes
115 else:
117 else:
116 if pf.startswith(b"'") and pf.endswith(b"'") and b" " in pf:
118 if pf.startswith(b"'") and pf.endswith(b"'") and b" " in pf:
117 pf = pf[1:-1] # Remove the quotes
119 pf = pf[1:-1] # Remove the quotes
118 return pf
120 return pf
119
121
120
122
121 def sshargs(
123 def sshargs(
122 sshcmd: bytes, host: bytes, user: Optional[bytes], port: Optional[bytes]
124 sshcmd: bytes, host: bytes, user: Optional[bytes], port: Optional[bytes]
123 ) -> bytes:
125 ) -> bytes:
124 '''Build argument list for ssh'''
126 '''Build argument list for ssh'''
125 args = user and (b"%s@%s" % (user, host)) or host
127 args = user and (b"%s@%s" % (user, host)) or host
126 if b'-' in args[:1]:
128 if b'-' in args[:1]:
127 raise error.Abort(
129 raise error.Abort(
128 _(b'illegal ssh hostname or username starting with -: %s') % args
130 _(b'illegal ssh hostname or username starting with -: %s') % args
129 )
131 )
130 args = shellquote(args)
132 args = shellquote(args)
131 if port:
133 if port:
132 args = b'-p %s %s' % (shellquote(port), args)
134 args = b'-p %s %s' % (shellquote(port), args)
133 return args
135 return args
134
136
135
137
136 def isexec(f: bytes) -> bool:
138 def isexec(f: bytes) -> bool:
137 """check whether a file is executable"""
139 """check whether a file is executable"""
138 return os.lstat(f).st_mode & 0o100 != 0
140 return os.lstat(f).st_mode & 0o100 != 0
139
141
140
142
141 def setflags(f: bytes, l: bool, x: bool) -> None:
143 def setflags(f: bytes, l: bool, x: bool) -> None:
142 st = os.lstat(f)
144 st = os.lstat(f)
143 s = st.st_mode
145 s = st.st_mode
144 if l:
146 if l:
145 if not stat.S_ISLNK(s):
147 if not stat.S_ISLNK(s):
146 # switch file to link
148 # switch file to link
147 with open(f, b'rb') as fp:
149 with open(f, b'rb') as fp:
148 data = fp.read()
150 data = fp.read()
149 unlink(f)
151 unlink(f)
150 try:
152 try:
151 os.symlink(data, f)
153 os.symlink(data, f)
152 except OSError:
154 except OSError:
153 # failed to make a link, rewrite file
155 # failed to make a link, rewrite file
154 with open(f, b"wb") as fp:
156 with open(f, b"wb") as fp:
155 fp.write(data)
157 fp.write(data)
156
158
157 # no chmod needed at this point
159 # no chmod needed at this point
158 return
160 return
159 if stat.S_ISLNK(s):
161 if stat.S_ISLNK(s):
160 # switch link to file
162 # switch link to file
161 data = os.readlink(f)
163 data = os.readlink(f)
162 unlink(f)
164 unlink(f)
163 with open(f, b"wb") as fp:
165 with open(f, b"wb") as fp:
164 fp.write(data)
166 fp.write(data)
165 s = 0o666 & ~umask # avoid restatting for chmod
167 s = 0o666 & ~umask # avoid restatting for chmod
166
168
167 sx = s & 0o100
169 sx = s & 0o100
168 if st.st_nlink > 1 and bool(x) != bool(sx):
170 if st.st_nlink > 1 and bool(x) != bool(sx):
169 # the file is a hardlink, break it
171 # the file is a hardlink, break it
170 with open(f, b"rb") as fp:
172 with open(f, b"rb") as fp:
171 data = fp.read()
173 data = fp.read()
172 unlink(f)
174 unlink(f)
173 with open(f, b"wb") as fp:
175 with open(f, b"wb") as fp:
174 fp.write(data)
176 fp.write(data)
175
177
176 if x and not sx:
178 if x and not sx:
177 # Turn on +x for every +r bit when making a file executable
179 # Turn on +x for every +r bit when making a file executable
178 # and obey umask.
180 # and obey umask.
179 os.chmod(f, s | (s & 0o444) >> 2 & ~umask)
181 os.chmod(f, s | (s & 0o444) >> 2 & ~umask)
180 elif not x and sx:
182 elif not x and sx:
181 # Turn off all +x bits
183 # Turn off all +x bits
182 os.chmod(f, s & 0o666)
184 os.chmod(f, s & 0o666)
183
185
184
186
185 def copymode(
187 def copymode(
186 src: bytes,
188 src: bytes,
187 dst: bytes,
189 dst: bytes,
188 mode: Optional[bytes] = None,
190 mode: Optional[bytes] = None,
189 enforcewritable: bool = False,
191 enforcewritable: bool = False,
190 ) -> None:
192 ) -> None:
191 """Copy the file mode from the file at path src to dst.
193 """Copy the file mode from the file at path src to dst.
192 If src doesn't exist, we're using mode instead. If mode is None, we're
194 If src doesn't exist, we're using mode instead. If mode is None, we're
193 using umask."""
195 using umask."""
194 try:
196 try:
195 st_mode = os.lstat(src).st_mode & 0o777
197 st_mode = os.lstat(src).st_mode & 0o777
196 except FileNotFoundError:
198 except FileNotFoundError:
197 st_mode = mode
199 st_mode = mode
198 if st_mode is None:
200 if st_mode is None:
199 st_mode = ~umask
201 st_mode = ~umask
200 st_mode &= 0o666
202 st_mode &= 0o666
201
203
202 new_mode = st_mode
204 new_mode = st_mode
203
205
204 if enforcewritable:
206 if enforcewritable:
205 new_mode |= stat.S_IWUSR
207 new_mode |= stat.S_IWUSR
206
208
207 os.chmod(dst, new_mode)
209 os.chmod(dst, new_mode)
208
210
209
211
210 def checkexec(path: bytes) -> bool:
212 def checkexec(path: bytes) -> bool:
211 """
213 """
212 Check whether the given path is on a filesystem with UNIX-like exec flags
214 Check whether the given path is on a filesystem with UNIX-like exec flags
213
215
214 Requires a directory (like /foo/.hg)
216 Requires a directory (like /foo/.hg)
215 """
217 """
216
218
217 # VFAT on some Linux versions can flip mode but it doesn't persist
219 # VFAT on some Linux versions can flip mode but it doesn't persist
218 # a FS remount. Frequently we can detect it if files are created
220 # a FS remount. Frequently we can detect it if files are created
219 # with exec bit on.
221 # with exec bit on.
220
222
221 try:
223 try:
222 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
224 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
223 basedir = os.path.join(path, b'.hg')
225 basedir = os.path.join(path, b'.hg')
224 cachedir = os.path.join(basedir, b'wcache')
226 cachedir = os.path.join(basedir, b'wcache')
225 storedir = os.path.join(basedir, b'store')
227 storedir = os.path.join(basedir, b'store')
226 if not os.path.exists(cachedir):
228 if not os.path.exists(cachedir):
227 try:
229 try:
228 # we want to create the 'cache' directory, not the '.hg' one.
230 # we want to create the 'cache' directory, not the '.hg' one.
229 # Automatically creating '.hg' directory could silently spawn
231 # Automatically creating '.hg' directory could silently spawn
230 # invalid Mercurial repositories. That seems like a bad idea.
232 # invalid Mercurial repositories. That seems like a bad idea.
231 os.mkdir(cachedir)
233 os.mkdir(cachedir)
232 if os.path.exists(storedir):
234 if os.path.exists(storedir):
233 copymode(storedir, cachedir)
235 copymode(storedir, cachedir)
234 else:
236 else:
235 copymode(basedir, cachedir)
237 copymode(basedir, cachedir)
236 except (IOError, OSError):
238 except (IOError, OSError):
237 # we other fallback logic triggers
239 # we other fallback logic triggers
238 pass
240 pass
239 if os.path.isdir(cachedir):
241 if os.path.isdir(cachedir):
240 checkisexec = os.path.join(cachedir, b'checkisexec')
242 checkisexec = os.path.join(cachedir, b'checkisexec')
241 checknoexec = os.path.join(cachedir, b'checknoexec')
243 checknoexec = os.path.join(cachedir, b'checknoexec')
242
244
243 try:
245 try:
244 m = os.stat(checkisexec).st_mode
246 m = os.stat(checkisexec).st_mode
245 except FileNotFoundError:
247 except FileNotFoundError:
246 # checkisexec does not exist - fall through ...
248 # checkisexec does not exist - fall through ...
247 pass
249 pass
248 else:
250 else:
249 # checkisexec exists, check if it actually is exec
251 # checkisexec exists, check if it actually is exec
250 if m & EXECFLAGS != 0:
252 if m & EXECFLAGS != 0:
251 # ensure checkisexec exists, check it isn't exec
253 # ensure checkisexec exists, check it isn't exec
252 try:
254 try:
253 m = os.stat(checknoexec).st_mode
255 m = os.stat(checknoexec).st_mode
254 except FileNotFoundError:
256 except FileNotFoundError:
255 open(checknoexec, b'w').close() # might fail
257 open(checknoexec, b'w').close() # might fail
256 m = os.stat(checknoexec).st_mode
258 m = os.stat(checknoexec).st_mode
257 if m & EXECFLAGS == 0:
259 if m & EXECFLAGS == 0:
258 # check-exec is exec and check-no-exec is not exec
260 # check-exec is exec and check-no-exec is not exec
259 return True
261 return True
260 # checknoexec exists but is exec - delete it
262 # checknoexec exists but is exec - delete it
261 unlink(checknoexec)
263 unlink(checknoexec)
262 # checkisexec exists but is not exec - delete it
264 # checkisexec exists but is not exec - delete it
263 unlink(checkisexec)
265 unlink(checkisexec)
264
266
265 # check using one file, leave it as checkisexec
267 # check using one file, leave it as checkisexec
266 checkdir = cachedir
268 checkdir = cachedir
267 else:
269 else:
268 # check directly in path and don't leave checkisexec behind
270 # check directly in path and don't leave checkisexec behind
269 checkdir = path
271 checkdir = path
270 checkisexec = None
272 checkisexec = None
271 fh, fn = pycompat.mkstemp(dir=checkdir, prefix=b'hg-checkexec-')
273 fh, fn = pycompat.mkstemp(dir=checkdir, prefix=b'hg-checkexec-')
272 try:
274 try:
273 os.close(fh)
275 os.close(fh)
274 m = os.stat(fn).st_mode
276 m = os.stat(fn).st_mode
275 if m & EXECFLAGS == 0:
277 if m & EXECFLAGS == 0:
276 os.chmod(fn, m & 0o777 | EXECFLAGS)
278 os.chmod(fn, m & 0o777 | EXECFLAGS)
277 if os.stat(fn).st_mode & EXECFLAGS != 0:
279 if os.stat(fn).st_mode & EXECFLAGS != 0:
278 if checkisexec is not None:
280 if checkisexec is not None:
279 os.rename(fn, checkisexec)
281 os.rename(fn, checkisexec)
280 fn = None
282 fn = None
281 return True
283 return True
282 finally:
284 finally:
283 if fn is not None:
285 if fn is not None:
284 unlink(fn)
286 unlink(fn)
285 except (IOError, OSError):
287 except (IOError, OSError):
286 # we don't care, the user probably won't be able to commit anyway
288 # we don't care, the user probably won't be able to commit anyway
287 return False
289 return False
288
290
289
291
290 def checklink(path: bytes) -> bool:
292 def checklink(path: bytes) -> bool:
291 """check whether the given path is on a symlink-capable filesystem"""
293 """check whether the given path is on a symlink-capable filesystem"""
292 # mktemp is not racy because symlink creation will fail if the
294 # mktemp is not racy because symlink creation will fail if the
293 # file already exists
295 # file already exists
294 while True:
296 while True:
295 cachedir = os.path.join(path, b'.hg', b'wcache')
297 cachedir = os.path.join(path, b'.hg', b'wcache')
296 checklink = os.path.join(cachedir, b'checklink')
298 checklink = os.path.join(cachedir, b'checklink')
297 # try fast path, read only
299 # try fast path, read only
298 if os.path.islink(checklink):
300 if os.path.islink(checklink):
299 return True
301 return True
300 if os.path.isdir(cachedir):
302 if os.path.isdir(cachedir):
301 checkdir = cachedir
303 checkdir = cachedir
302 else:
304 else:
303 checkdir = path
305 checkdir = path
304 cachedir = None
306 cachedir = None
305 name = tempfile.mktemp(
307 name = tempfile.mktemp(
306 dir=pycompat.fsdecode(checkdir), prefix=r'checklink-'
308 dir=pycompat.fsdecode(checkdir), prefix=r'checklink-'
307 )
309 )
308 name = pycompat.fsencode(name)
310 name = pycompat.fsencode(name)
309 try:
311 try:
310 fd = None
312 fd = None
311 if cachedir is None:
313 if cachedir is None:
312 fd = pycompat.namedtempfile(
314 fd = pycompat.namedtempfile(
313 dir=checkdir, prefix=b'hg-checklink-'
315 dir=checkdir, prefix=b'hg-checklink-'
314 )
316 )
315 target = os.path.basename(fd.name)
317 target = os.path.basename(fd.name)
316 else:
318 else:
317 # create a fixed file to link to; doesn't matter if it
319 # create a fixed file to link to; doesn't matter if it
318 # already exists.
320 # already exists.
319 target = b'checklink-target'
321 target = b'checklink-target'
320 try:
322 try:
321 fullpath = os.path.join(cachedir, target)
323 fullpath = os.path.join(cachedir, target)
322 open(fullpath, b'w').close()
324 open(fullpath, b'w').close()
323 except PermissionError:
325 except PermissionError:
324 # If we can't write to cachedir, just pretend
326 # If we can't write to cachedir, just pretend
325 # that the fs is readonly and by association
327 # that the fs is readonly and by association
326 # that the fs won't support symlinks. This
328 # that the fs won't support symlinks. This
327 # seems like the least dangerous way to avoid
329 # seems like the least dangerous way to avoid
328 # data loss.
330 # data loss.
329 return False
331 return False
330 try:
332 try:
331 os.symlink(target, name)
333 os.symlink(target, name)
332 if cachedir is None:
334 if cachedir is None:
333 unlink(name)
335 unlink(name)
334 else:
336 else:
335 try:
337 try:
336 os.rename(name, checklink)
338 os.rename(name, checklink)
337 except OSError:
339 except OSError:
338 unlink(name)
340 unlink(name)
339 return True
341 return True
340 except FileExistsError:
342 except FileExistsError:
341 # link creation might race, try again
343 # link creation might race, try again
342 continue
344 continue
343 finally:
345 finally:
344 if fd is not None:
346 if fd is not None:
345 fd.close()
347 fd.close()
346 except AttributeError:
348 except AttributeError:
347 return False
349 return False
348 except OSError as inst:
350 except OSError as inst:
349 # sshfs might report failure while successfully creating the link
351 # sshfs might report failure while successfully creating the link
350 if inst.errno == errno.EIO and os.path.exists(name):
352 if inst.errno == errno.EIO and os.path.exists(name):
351 unlink(name)
353 unlink(name)
352 return False
354 return False
353
355
354
356
355 def checkosfilename(path):
357 def checkosfilename(path: bytes) -> Optional[bytes]:
356 """Check that the base-relative path is a valid filename on this platform.
358 """Check that the base-relative path is a valid filename on this platform.
357 Returns None if the path is ok, or a UI string describing the problem."""
359 Returns None if the path is ok, or a UI string describing the problem."""
358 return None # on posix platforms, every path is ok
360 return None # on posix platforms, every path is ok
359
361
360
362
361 def getfsmountpoint(dirpath):
363 def getfsmountpoint(dirpath: bytes) -> Optional[bytes]:
362 """Get the filesystem mount point from a directory (best-effort)
364 """Get the filesystem mount point from a directory (best-effort)
363
365
364 Returns None if we are unsure. Raises OSError on ENOENT, EPERM, etc.
366 Returns None if we are unsure. Raises OSError on ENOENT, EPERM, etc.
365 """
367 """
366 return getattr(osutil, 'getfsmountpoint', lambda x: None)(dirpath)
368 return getattr(osutil, 'getfsmountpoint', lambda x: None)(dirpath)
367
369
368
370
369 def getfstype(dirpath: bytes) -> Optional[bytes]:
371 def getfstype(dirpath: bytes) -> Optional[bytes]:
370 """Get the filesystem type name from a directory (best-effort)
372 """Get the filesystem type name from a directory (best-effort)
371
373
372 Returns None if we are unsure. Raises OSError on ENOENT, EPERM, etc.
374 Returns None if we are unsure. Raises OSError on ENOENT, EPERM, etc.
373 """
375 """
374 return getattr(osutil, 'getfstype', lambda x: None)(dirpath)
376 return getattr(osutil, 'getfstype', lambda x: None)(dirpath)
375
377
376
378
377 def get_password() -> bytes:
379 def get_password() -> bytes:
378 return encoding.strtolocal(getpass.getpass(''))
380 return encoding.strtolocal(getpass.getpass(''))
379
381
380
382
381 def setbinary(fd) -> None:
383 def setbinary(fd) -> None:
382 pass
384 pass
383
385
384
386
385 def pconvert(path: bytes) -> bytes:
387 def pconvert(path: bytes) -> bytes:
386 return path
388 return path
387
389
388
390
389 def localpath(path: bytes) -> bytes:
391 def localpath(path: bytes) -> bytes:
390 return path
392 return path
391
393
392
394
393 def samefile(fpath1: bytes, fpath2: bytes) -> bool:
395 def samefile(fpath1: bytes, fpath2: bytes) -> bool:
394 """Returns whether path1 and path2 refer to the same file. This is only
396 """Returns whether path1 and path2 refer to the same file. This is only
395 guaranteed to work for files, not directories."""
397 guaranteed to work for files, not directories."""
396 return os.path.samefile(fpath1, fpath2)
398 return os.path.samefile(fpath1, fpath2)
397
399
398
400
399 def samedevice(fpath1: bytes, fpath2: bytes) -> bool:
401 def samedevice(fpath1: bytes, fpath2: bytes) -> bool:
400 """Returns whether fpath1 and fpath2 are on the same device. This is only
402 """Returns whether fpath1 and fpath2 are on the same device. This is only
401 guaranteed to work for files, not directories."""
403 guaranteed to work for files, not directories."""
402 st1 = os.lstat(fpath1)
404 st1 = os.lstat(fpath1)
403 st2 = os.lstat(fpath2)
405 st2 = os.lstat(fpath2)
404 return st1.st_dev == st2.st_dev
406 return st1.st_dev == st2.st_dev
405
407
406
408
407 # os.path.normcase is a no-op, which doesn't help us on non-native filesystems
409 # os.path.normcase is a no-op, which doesn't help us on non-native filesystems
408 def normcase(path: bytes) -> bytes:
410 def normcase(path: bytes) -> bytes:
409 return path.lower()
411 return path.lower()
410
412
411
413
412 # what normcase does to ASCII strings
414 # what normcase does to ASCII strings
413 normcasespec = encoding.normcasespecs.lower
415 normcasespec: int = encoding.normcasespecs.lower
414 # fallback normcase function for non-ASCII strings
416 # fallback normcase function for non-ASCII strings
415 normcasefallback = normcase
417 normcasefallback = normcase
416
418
417 if pycompat.isdarwin:
419 if pycompat.isdarwin:
418
420
419 def normcase(path: bytes) -> bytes:
421 def normcase(path: bytes) -> bytes:
420 """
422 """
421 Normalize a filename for OS X-compatible comparison:
423 Normalize a filename for OS X-compatible comparison:
422 - escape-encode invalid characters
424 - escape-encode invalid characters
423 - decompose to NFD
425 - decompose to NFD
424 - lowercase
426 - lowercase
425 - omit ignored characters [200c-200f, 202a-202e, 206a-206f,feff]
427 - omit ignored characters [200c-200f, 202a-202e, 206a-206f,feff]
426
428
427 >>> normcase(b'UPPER')
429 >>> normcase(b'UPPER')
428 'upper'
430 'upper'
429 >>> normcase(b'Caf\\xc3\\xa9')
431 >>> normcase(b'Caf\\xc3\\xa9')
430 'cafe\\xcc\\x81'
432 'cafe\\xcc\\x81'
431 >>> normcase(b'\\xc3\\x89')
433 >>> normcase(b'\\xc3\\x89')
432 'e\\xcc\\x81'
434 'e\\xcc\\x81'
433 >>> normcase(b'\\xb8\\xca\\xc3\\xca\\xbe\\xc8.JPG') # issue3918
435 >>> normcase(b'\\xb8\\xca\\xc3\\xca\\xbe\\xc8.JPG') # issue3918
434 '%b8%ca%c3\\xca\\xbe%c8.jpg'
436 '%b8%ca%c3\\xca\\xbe%c8.jpg'
435 """
437 """
436
438
437 try:
439 try:
438 return encoding.asciilower(path) # exception for non-ASCII
440 return encoding.asciilower(path) # exception for non-ASCII
439 except UnicodeDecodeError:
441 except UnicodeDecodeError:
440 return normcasefallback(path)
442 return normcasefallback(path)
441
443
442 normcasespec = encoding.normcasespecs.lower
444 normcasespec = encoding.normcasespecs.lower
443
445
444 def normcasefallback(path: bytes) -> bytes:
446 def normcasefallback(path: bytes) -> bytes:
445 try:
447 try:
446 u = path.decode('utf-8')
448 u = path.decode('utf-8')
447 except UnicodeDecodeError:
449 except UnicodeDecodeError:
448 # OS X percent-encodes any bytes that aren't valid utf-8
450 # OS X percent-encodes any bytes that aren't valid utf-8
449 s = b''
451 s = b''
450 pos = 0
452 pos = 0
451 l = len(path)
453 l = len(path)
452 while pos < l:
454 while pos < l:
453 try:
455 try:
454 c = encoding.getutf8char(path, pos)
456 c = encoding.getutf8char(path, pos)
455 pos += len(c)
457 pos += len(c)
456 except ValueError:
458 except ValueError:
457 c = b'%%%02X' % ord(path[pos : pos + 1])
459 c = b'%%%02X' % ord(path[pos : pos + 1])
458 pos += 1
460 pos += 1
459 s += c
461 s += c
460
462
461 u = s.decode('utf-8')
463 u = s.decode('utf-8')
462
464
463 # Decompose then lowercase (HFS+ technote specifies lower)
465 # Decompose then lowercase (HFS+ technote specifies lower)
464 enc = unicodedata.normalize('NFD', u).lower().encode('utf-8')
466 enc = unicodedata.normalize('NFD', u).lower().encode('utf-8')
465 # drop HFS+ ignored characters
467 # drop HFS+ ignored characters
466 return encoding.hfsignoreclean(enc)
468 return encoding.hfsignoreclean(enc)
467
469
468
470
469 if pycompat.sysplatform == b'cygwin':
471 if pycompat.sysplatform == b'cygwin':
470 # workaround for cygwin, in which mount point part of path is
472 # workaround for cygwin, in which mount point part of path is
471 # treated as case sensitive, even though underlying NTFS is case
473 # treated as case sensitive, even though underlying NTFS is case
472 # insensitive.
474 # insensitive.
473
475
474 # default mount points
476 # default mount points
475 cygwinmountpoints = sorted(
477 cygwinmountpoints = sorted(
476 [
478 [
477 b"/usr/bin",
479 b"/usr/bin",
478 b"/usr/lib",
480 b"/usr/lib",
479 b"/cygdrive",
481 b"/cygdrive",
480 ],
482 ],
481 reverse=True,
483 reverse=True,
482 )
484 )
483
485
484 # use upper-ing as normcase as same as NTFS workaround
486 # use upper-ing as normcase as same as NTFS workaround
485 def normcase(path: bytes) -> bytes:
487 def normcase(path: bytes) -> bytes:
486 pathlen = len(path)
488 pathlen = len(path)
487 if (pathlen == 0) or (path[0] != pycompat.ossep):
489 if (pathlen == 0) or (path[0] != pycompat.ossep):
488 # treat as relative
490 # treat as relative
489 return encoding.upper(path)
491 return encoding.upper(path)
490
492
491 # to preserve case of mountpoint part
493 # to preserve case of mountpoint part
492 for mp in cygwinmountpoints:
494 for mp in cygwinmountpoints:
493 if not path.startswith(mp):
495 if not path.startswith(mp):
494 continue
496 continue
495
497
496 mplen = len(mp)
498 mplen = len(mp)
497 if mplen == pathlen: # mount point itself
499 if mplen == pathlen: # mount point itself
498 return mp
500 return mp
499 if path[mplen] == pycompat.ossep:
501 if path[mplen] == pycompat.ossep:
500 return mp + encoding.upper(path[mplen:])
502 return mp + encoding.upper(path[mplen:])
501
503
502 return encoding.upper(path)
504 return encoding.upper(path)
503
505
504 normcasespec = encoding.normcasespecs.other
506 normcasespec = encoding.normcasespecs.other
505 normcasefallback = normcase
507 normcasefallback = normcase
506
508
507 # Cygwin translates native ACLs to POSIX permissions,
509 # Cygwin translates native ACLs to POSIX permissions,
508 # but these translations are not supported by native
510 # but these translations are not supported by native
509 # tools, so the exec bit tends to be set erroneously.
511 # tools, so the exec bit tends to be set erroneously.
510 # Therefore, disable executable bit access on Cygwin.
512 # Therefore, disable executable bit access on Cygwin.
511 def checkexec(path: bytes) -> bool:
513 def checkexec(path: bytes) -> bool:
512 return False
514 return False
513
515
514 # Similarly, Cygwin's symlink emulation is likely to create
516 # Similarly, Cygwin's symlink emulation is likely to create
515 # problems when Mercurial is used from both Cygwin and native
517 # problems when Mercurial is used from both Cygwin and native
516 # Windows, with other native tools, or on shared volumes
518 # Windows, with other native tools, or on shared volumes
517 def checklink(path: bytes) -> bool:
519 def checklink(path: bytes) -> bool:
518 return False
520 return False
519
521
520
522
521 _needsshellquote = None
523 _needsshellquote: Optional[Match[bytes]] = None
522
524
523
525
524 def shellquote(s: bytes) -> bytes:
526 def shellquote(s: bytes) -> bytes:
525 if pycompat.sysplatform == b'OpenVMS':
527 if pycompat.sysplatform == b'OpenVMS':
526 return b'"%s"' % s
528 return b'"%s"' % s
527 global _needsshellquote
529 global _needsshellquote
528 if _needsshellquote is None:
530 if _needsshellquote is None:
529 _needsshellquote = re.compile(br'[^a-zA-Z0-9._/+-]').search
531 _needsshellquote = re.compile(br'[^a-zA-Z0-9._/+-]').search
530 if s and not _needsshellquote(s):
532 if s and not _needsshellquote(s):
531 # "s" shouldn't have to be quoted
533 # "s" shouldn't have to be quoted
532 return s
534 return s
533 else:
535 else:
534 return b"'%s'" % s.replace(b"'", b"'\\''")
536 return b"'%s'" % s.replace(b"'", b"'\\''")
535
537
536
538
537 def shellsplit(s: bytes) -> List[bytes]:
539 def shellsplit(s: bytes) -> List[bytes]:
538 """Parse a command string in POSIX shell way (best-effort)"""
540 """Parse a command string in POSIX shell way (best-effort)"""
539 return pycompat.shlexsplit(s, posix=True)
541 return pycompat.shlexsplit(s, posix=True)
540
542
541
543
542 def testpid(pid: int) -> bool:
544 def testpid(pid: int) -> bool:
543 '''return False if pid dead, True if running or not sure'''
545 '''return False if pid dead, True if running or not sure'''
544 if pycompat.sysplatform == b'OpenVMS':
546 if pycompat.sysplatform == b'OpenVMS':
545 return True
547 return True
546 try:
548 try:
547 os.kill(pid, 0)
549 os.kill(pid, 0)
548 return True
550 return True
549 except OSError as inst:
551 except OSError as inst:
550 return inst.errno != errno.ESRCH
552 return inst.errno != errno.ESRCH
551
553
552
554
553 def isowner(st: os.stat_result) -> bool:
555 def isowner(st: os.stat_result) -> bool:
554 """Return True if the stat object st is from the current user."""
556 """Return True if the stat object st is from the current user."""
555 return st.st_uid == os.getuid()
557 return st.st_uid == os.getuid()
556
558
557
559
558 def findexe(command: bytes) -> Optional[bytes]:
560 def findexe(command: bytes) -> Optional[bytes]:
559 """Find executable for command searching like which does.
561 """Find executable for command searching like which does.
560 If command is a basename then PATH is searched for command.
562 If command is a basename then PATH is searched for command.
561 PATH isn't searched if command is an absolute or relative path.
563 PATH isn't searched if command is an absolute or relative path.
562 If command isn't found None is returned."""
564 If command isn't found None is returned."""
563 if pycompat.sysplatform == b'OpenVMS':
565 if pycompat.sysplatform == b'OpenVMS':
564 return command
566 return command
565
567
566 def findexisting(executable: bytes) -> Optional[bytes]:
568 def findexisting(executable: bytes) -> Optional[bytes]:
567 b'Will return executable if existing file'
569 b'Will return executable if existing file'
568 if os.path.isfile(executable) and os.access(executable, os.X_OK):
570 if os.path.isfile(executable) and os.access(executable, os.X_OK):
569 return executable
571 return executable
570 return None
572 return None
571
573
572 if pycompat.ossep in command:
574 if pycompat.ossep in command:
573 return findexisting(command)
575 return findexisting(command)
574
576
575 if pycompat.sysplatform == b'plan9':
577 if pycompat.sysplatform == b'plan9':
576 return findexisting(os.path.join(b'/bin', command))
578 return findexisting(os.path.join(b'/bin', command))
577
579
578 for path in encoding.environ.get(b'PATH', b'').split(pycompat.ospathsep):
580 for path in encoding.environ.get(b'PATH', b'').split(pycompat.ospathsep):
579 executable = findexisting(os.path.join(path, command))
581 executable = findexisting(os.path.join(path, command))
580 if executable is not None:
582 if executable is not None:
581 return executable
583 return executable
582 return None
584 return None
583
585
584
586
585 def setsignalhandler() -> None:
587 def setsignalhandler() -> None:
586 pass
588 pass
587
589
588
590
589 _wantedkinds = {stat.S_IFREG, stat.S_IFLNK}
591 _wantedkinds = {stat.S_IFREG, stat.S_IFLNK}
590
592
591
593
592 def statfiles(files: Sequence[bytes]) -> Iterator[Optional[os.stat_result]]:
594 def statfiles(files: Sequence[bytes]) -> Iterator[Optional[os.stat_result]]:
593 """Stat each file in files. Yield each stat, or None if a file does not
595 """Stat each file in files. Yield each stat, or None if a file does not
594 exist or has a type we don't care about."""
596 exist or has a type we don't care about."""
595 lstat = os.lstat
597 lstat = os.lstat
596 getkind = stat.S_IFMT
598 getkind = stat.S_IFMT
597 for nf in files:
599 for nf in files:
598 try:
600 try:
599 st = lstat(nf)
601 st = lstat(nf)
600 if getkind(st.st_mode) not in _wantedkinds:
602 if getkind(st.st_mode) not in _wantedkinds:
601 st = None
603 st = None
602 except (FileNotFoundError, NotADirectoryError):
604 except (FileNotFoundError, NotADirectoryError):
603 st = None
605 st = None
604 yield st
606 yield st
605
607
606
608
607 def getuser() -> bytes:
609 def getuser() -> bytes:
608 '''return name of current user'''
610 '''return name of current user'''
609 return pycompat.fsencode(getpass.getuser())
611 return pycompat.fsencode(getpass.getuser())
610
612
611
613
612 def username(uid: Optional[int] = None) -> Optional[bytes]:
614 def username(uid: Optional[int] = None) -> Optional[bytes]:
613 """Return the name of the user with the given uid.
615 """Return the name of the user with the given uid.
614
616
615 If uid is None, return the name of the current user."""
617 If uid is None, return the name of the current user."""
616
618
617 if uid is None:
619 if uid is None:
618 uid = os.getuid()
620 uid = os.getuid()
619 try:
621 try:
620 return pycompat.fsencode(pwd.getpwuid(uid)[0])
622 return pycompat.fsencode(pwd.getpwuid(uid)[0])
621 except KeyError:
623 except KeyError:
622 return b'%d' % uid
624 return b'%d' % uid
623
625
624
626
625 def groupname(gid: Optional[int] = None) -> Optional[bytes]:
627 def groupname(gid: Optional[int] = None) -> Optional[bytes]:
626 """Return the name of the group with the given gid.
628 """Return the name of the group with the given gid.
627
629
628 If gid is None, return the name of the current group."""
630 If gid is None, return the name of the current group."""
629
631
630 if gid is None:
632 if gid is None:
631 gid = os.getgid()
633 gid = os.getgid()
632 try:
634 try:
633 return pycompat.fsencode(grp.getgrgid(gid)[0])
635 return pycompat.fsencode(grp.getgrgid(gid)[0])
634 except KeyError:
636 except KeyError:
635 return pycompat.bytestr(gid)
637 return pycompat.bytestr(gid)
636
638
637
639
638 def groupmembers(name: bytes) -> List[bytes]:
640 def groupmembers(name: bytes) -> List[bytes]:
639 """Return the list of members of the group with the given
641 """Return the list of members of the group with the given
640 name, KeyError if the group does not exist.
642 name, KeyError if the group does not exist.
641 """
643 """
642 name = pycompat.fsdecode(name)
644 name = pycompat.fsdecode(name)
643 return pycompat.rapply(pycompat.fsencode, list(grp.getgrnam(name).gr_mem))
645 return pycompat.rapply(pycompat.fsencode, list(grp.getgrnam(name).gr_mem))
644
646
645
647
646 def spawndetached(args: List[bytes]) -> int:
648 def spawndetached(args: List[bytes]) -> int:
647 return os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0), args[0], args)
649 return os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0), args[0], args)
648
650
649
651
650 def gethgcmd():
652 def gethgcmd(): # TODO: convert to bytes, like on Windows?
651 return sys.argv[:1]
653 return sys.argv[:1]
652
654
653
655
654 def makedir(path: bytes, notindexed: bool) -> None:
656 def makedir(path: bytes, notindexed: bool) -> None:
655 os.mkdir(path)
657 os.mkdir(path)
656
658
657
659
658 def lookupreg(
660 def lookupreg(
659 key: bytes,
661 key: bytes,
660 name: Optional[bytes] = None,
662 name: Optional[bytes] = None,
661 scope: Optional[Union[int, Iterable[int]]] = None,
663 scope: Optional[Union[int, Iterable[int]]] = None,
662 ) -> Optional[bytes]:
664 ) -> Optional[bytes]:
663 return None
665 return None
664
666
665
667
666 def hidewindow() -> None:
668 def hidewindow() -> None:
667 """Hide current shell window.
669 """Hide current shell window.
668
670
669 Used to hide the window opened when starting asynchronous
671 Used to hide the window opened when starting asynchronous
670 child process under Windows, unneeded on other systems.
672 child process under Windows, unneeded on other systems.
671 """
673 """
672 pass
674 pass
673
675
674
676
675 class cachestat:
677 class cachestat:
676 def __init__(self, path: bytes) -> None:
678 def __init__(self, path: bytes) -> None:
677 self.stat = os.stat(path)
679 self.stat = os.stat(path)
678
680
679 def cacheable(self) -> bool:
681 def cacheable(self) -> bool:
680 return bool(self.stat.st_ino)
682 return bool(self.stat.st_ino)
681
683
682 __hash__ = object.__hash__
684 __hash__ = object.__hash__
683
685
684 def __eq__(self, other: Any) -> bool:
686 def __eq__(self, other: Any) -> bool:
685 try:
687 try:
686 # Only dev, ino, size, mtime and atime are likely to change. Out
688 # Only dev, ino, size, mtime and atime are likely to change. Out
687 # of these, we shouldn't compare atime but should compare the
689 # of these, we shouldn't compare atime but should compare the
688 # rest. However, one of the other fields changing indicates
690 # rest. However, one of the other fields changing indicates
689 # something fishy going on, so return False if anything but atime
691 # something fishy going on, so return False if anything but atime
690 # changes.
692 # changes.
691 return (
693 return (
692 self.stat.st_mode == other.stat.st_mode
694 self.stat.st_mode == other.stat.st_mode
693 and self.stat.st_ino == other.stat.st_ino
695 and self.stat.st_ino == other.stat.st_ino
694 and self.stat.st_dev == other.stat.st_dev
696 and self.stat.st_dev == other.stat.st_dev
695 and self.stat.st_nlink == other.stat.st_nlink
697 and self.stat.st_nlink == other.stat.st_nlink
696 and self.stat.st_uid == other.stat.st_uid
698 and self.stat.st_uid == other.stat.st_uid
697 and self.stat.st_gid == other.stat.st_gid
699 and self.stat.st_gid == other.stat.st_gid
698 and self.stat.st_size == other.stat.st_size
700 and self.stat.st_size == other.stat.st_size
699 and self.stat[stat.ST_MTIME] == other.stat[stat.ST_MTIME]
701 and self.stat[stat.ST_MTIME] == other.stat[stat.ST_MTIME]
700 and self.stat[stat.ST_CTIME] == other.stat[stat.ST_CTIME]
702 and self.stat[stat.ST_CTIME] == other.stat[stat.ST_CTIME]
701 )
703 )
702 except AttributeError:
704 except AttributeError:
703 return False
705 return False
704
706
705 def __ne__(self, other: Any) -> bool:
707 def __ne__(self, other: Any) -> bool:
706 return not self == other
708 return not self == other
707
709
708
710
709 def statislink(st: Optional[os.stat_result]) -> bool:
711 def statislink(st: Optional[os.stat_result]) -> bool:
710 '''check whether a stat result is a symlink'''
712 '''check whether a stat result is a symlink'''
711 return stat.S_ISLNK(st.st_mode) if st else False
713 return stat.S_ISLNK(st.st_mode) if st else False
712
714
713
715
714 def statisexec(st: Optional[os.stat_result]) -> bool:
716 def statisexec(st: Optional[os.stat_result]) -> bool:
715 '''check whether a stat result is an executable file'''
717 '''check whether a stat result is an executable file'''
716 return (st.st_mode & 0o100 != 0) if st else False
718 return (st.st_mode & 0o100 != 0) if st else False
717
719
718
720
719 def poll(fds):
721 def poll(fds):
720 """block until something happens on any file descriptor
722 """block until something happens on any file descriptor
721
723
722 This is a generic helper that will check for any activity
724 This is a generic helper that will check for any activity
723 (read, write. exception) and return the list of touched files.
725 (read, write. exception) and return the list of touched files.
724
726
725 In unsupported cases, it will raise a NotImplementedError"""
727 In unsupported cases, it will raise a NotImplementedError"""
726 try:
728 try:
727 res = select.select(fds, fds, fds)
729 res = select.select(fds, fds, fds)
728 except ValueError: # out of range file descriptor
730 except ValueError: # out of range file descriptor
729 raise NotImplementedError()
731 raise NotImplementedError()
730 return sorted(list(set(sum(res, []))))
732 return sorted(list(set(sum(res, []))))
731
733
732
734
733 def readpipe(pipe) -> bytes:
735 def readpipe(pipe) -> bytes:
734 """Read all available data from a pipe."""
736 """Read all available data from a pipe."""
735 # We can't fstat() a pipe because Linux will always report 0.
737 # We can't fstat() a pipe because Linux will always report 0.
736 # So, we set the pipe to non-blocking mode and read everything
738 # So, we set the pipe to non-blocking mode and read everything
737 # that's available.
739 # that's available.
738 flags = fcntl.fcntl(pipe, fcntl.F_GETFL)
740 flags = fcntl.fcntl(pipe, fcntl.F_GETFL)
739 flags |= os.O_NONBLOCK
741 flags |= os.O_NONBLOCK
740 oldflags = fcntl.fcntl(pipe, fcntl.F_SETFL, flags)
742 oldflags = fcntl.fcntl(pipe, fcntl.F_SETFL, flags)
741
743
742 try:
744 try:
743 chunks = []
745 chunks = []
744 while True:
746 while True:
745 try:
747 try:
746 s = pipe.read()
748 s = pipe.read()
747 if not s:
749 if not s:
748 break
750 break
749 chunks.append(s)
751 chunks.append(s)
750 except IOError:
752 except IOError:
751 break
753 break
752
754
753 return b''.join(chunks)
755 return b''.join(chunks)
754 finally:
756 finally:
755 fcntl.fcntl(pipe, fcntl.F_SETFL, oldflags)
757 fcntl.fcntl(pipe, fcntl.F_SETFL, oldflags)
756
758
757
759
758 def bindunixsocket(sock, path: bytes) -> None:
760 def bindunixsocket(sock, path: bytes) -> None:
759 """Bind the UNIX domain socket to the specified path"""
761 """Bind the UNIX domain socket to the specified path"""
760 # use relative path instead of full path at bind() if possible, since
762 # use relative path instead of full path at bind() if possible, since
761 # AF_UNIX path has very small length limit (107 chars) on common
763 # AF_UNIX path has very small length limit (107 chars) on common
762 # platforms (see sys/un.h)
764 # platforms (see sys/un.h)
763 dirname, basename = os.path.split(path)
765 dirname, basename = os.path.split(path)
764 bakwdfd = None
766 bakwdfd = None
765
767
766 try:
768 try:
767 if dirname:
769 if dirname:
768 bakwdfd = os.open(b'.', os.O_DIRECTORY)
770 bakwdfd = os.open(b'.', os.O_DIRECTORY)
769 os.chdir(dirname)
771 os.chdir(dirname)
770 sock.bind(basename)
772 sock.bind(basename)
771 if bakwdfd:
773 if bakwdfd:
772 os.fchdir(bakwdfd)
774 os.fchdir(bakwdfd)
773 finally:
775 finally:
774 if bakwdfd:
776 if bakwdfd:
775 os.close(bakwdfd)
777 os.close(bakwdfd)
General Comments 0
You need to be logged in to leave comments. Login now