blob: 47a8bbacead1a15f6d635d7e8c3cda60a57c4240 [file] [log] [blame]
#!/usr/bin/env python
'''
Mercurial extension to enable purging long filenames on Windows
It's possible to have filenaems that exceed the MAX_PATH limit (256 characters)
on Windows, rendering them un-purgeable with the regular python API calls.
To work around this limitation, we can use the DeleteFileW API
(https://msdn.microsoft.com/en-us/library/windows/desktop/aa363915%28v=vs.85%29.aspx)
and prefix the filename with \\?\.
This extension needs to monkeypatch other modules in order to function. It
attempts to be very conservative, and only applies the patches for the
duration of the purge() command. The original functions are restored after
the purge() command exits.
'''
from contextlib import contextmanager
from functools import partial
import os
import errno
import mercurial.extensions
import mercurial.util
if os.name == 'nt':
import ctypes
# Get a reference to the DeleteFileW function
# DeleteFileW accepts filenames encoded as a null terminated sequence of
# wide chars (UTF-16). Python's ctypes.c_wchar_p correctly encodes unicode
# strings to null terminated UTF-16 strings.
# However, we receive (byte) strings from mercurial. When these are passed
# to DeleteFileW via the c_wchar_p type, they are implicitly decoded via
# the 'mbcs' encoding on windows.
kernel32 = ctypes.windll.kernel32
DeleteFile = kernel32.DeleteFileW
DeleteFile.argtypes = [ctypes.c_wchar_p]
DeleteFile.restype = ctypes.c_bool
def unlink_long(fn):
normalized_path = '\\\\?\\' + os.path.normpath(fn)
if not DeleteFile(normalized_path):
raise OSError(errno.EPERM, "couldn't remove long path", fn)
# Not needed on other platforms, but is handy for testing
else:
def unlink_long(fn):
os.unlink(fn)
def unlink_wrapper(unlink_orig, fn, ui):
'''Calls the original unlink function, and if that fails, calls
unlink_long'''
try:
ui.debug('calling unlink_orig %s\n' % fn)
return unlink_orig(fn)
except WindowsError, e:
# windows error 3 corresponds to ERROR_PATH_NOT_FOUND
# only handle this case; re-raise the exception for other kinds of
# failures
if e.winerror != 3:
raise
ui.debug('caught WindowsError ERROR_PATH_NOT_FOUND; '
'calling unlink_long %s\n' % fn)
return unlink_long(fn)
@contextmanager
def wrap_unlink(ui):
'''Context manager that patches the required functions that are used by
the purge extension to remove files. When exiting the context manager
the original functions are restored.'''
version = mercurial.util.version()
if version >= '3.2':
# hg 3.2 and higher use util.unlink for purging
purgemod = mercurial.extensions.find('purge')
to_wrap = [(purgemod.util, 'unlink')]
else:
# hg 3.1 and earlier use os.remove directly
to_wrap = [(os, 'remove')]
# pass along the ui object to the unlink_wrapper so we can get logging out
# of it
wrapped = partial(unlink_wrapper, ui=ui)
# Wrap the original function(s) with our unlink_wrapper
originals = {}
for mod, func in to_wrap:
ui.debug('wrapping %s %s\n' % (mod, func))
originals[mod, func] = mercurial.extensions.wrapfunction(
mod, func, wrapped)
try:
yield
finally:
# Restore the originals
for mod, func in to_wrap:
ui.debug('restoring %s %s\n' % (mod, func))
setattr(mod, func, originals[mod, func])
def purge_wrapper(orig, ui, *args, **kwargs):
'''Runs the original purge() command inside the wrap_unlink() context
manager.'''
with wrap_unlink(ui):
return orig(ui, *args, **kwargs)
def extsetup(ui):
try:
purgemod = mercurial.extensions.find('purge')
except KeyError:
ui.warn('purge extension not found; '
'not enabling long filename support\n')
return
ui.note('enabling long filename support for purge\n')
mercurial.extensions.wrapcommand(purgemod.cmdtable, 'purge', purge_wrapper)