Skip to content
This repository has been archived by the owner on Nov 9, 2017. It is now read-only.

Support long paths #117

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Documentation/config.txt
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,13 @@ core.fscache::
Git for Windows uses this to bulk-read and cache lstat data of entire
directories (instead of doing lstat file by file).

core.longpaths::
Enable long path (> 260) support for builtin commands in Git for
Windows. This is disabled by default, as long paths are not supported
by Windows Explorer, cmd.exe and the Git for Windows tool chain
(msys, bash, tcl, perl...). Only enable this if you know what you're
doing and are prepared to live with a few quirks.

core.createObject::
You can set this to 'link', in which case a hardlink followed by
a delete of the source are used to make sure that object creation
Expand Down
2 changes: 2 additions & 0 deletions cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,8 @@ extern enum hide_dotfiles_type hide_dotfiles;

extern int core_fscache;

extern int core_long_paths;

enum branch_track {
BRANCH_TRACK_UNSPECIFIED = -1,
BRANCH_TRACK_NEVER = 0,
Expand Down
129 changes: 100 additions & 29 deletions compat/mingw.c
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,8 @@ static int ask_yes_no_if_possible(const char *format, ...)
int mingw_unlink(const char *pathname)
{
int ret, tries = 0;
wchar_t wpathname[MAX_PATH];
if (xutftowcs_path(wpathname, pathname) < 0)
wchar_t wpathname[MAX_LONG_PATH];
if (xutftowcs_long_path(wpathname, pathname) < 0)
return -1;

/* read-only files cannot be removed */
Expand Down Expand Up @@ -234,7 +234,7 @@ static int is_dir_empty(const wchar_t *wpath)
{
WIN32_FIND_DATAW findbuf;
HANDLE handle;
wchar_t wbuf[MAX_PATH + 2];
wchar_t wbuf[MAX_LONG_PATH + 2];
wcscpy(wbuf, wpath);
wcscat(wbuf, L"\\*");
handle = FindFirstFileW(wbuf, &findbuf);
Expand All @@ -255,8 +255,8 @@ static int is_dir_empty(const wchar_t *wpath)
int mingw_rmdir(const char *pathname)
{
int ret, tries = 0;
wchar_t wpathname[MAX_PATH];
if (xutftowcs_path(wpathname, pathname) < 0)
wchar_t wpathname[MAX_LONG_PATH];
if (xutftowcs_long_path(wpathname, pathname) < 0)
return -1;

while ((ret = _wrmdir(wpathname)) == -1 && tries < ARRAY_SIZE(delay)) {
Expand Down Expand Up @@ -296,9 +296,9 @@ static int make_hidden(const wchar_t *path)

void mingw_mark_as_git_dir(const char *dir)
{
wchar_t wdir[MAX_PATH];
wchar_t wdir[MAX_LONG_PATH];
if (hide_dotfiles != HIDE_DOTFILES_FALSE && !is_bare_repository())
if (xutftowcs_path(wdir, dir) < 0 || make_hidden(wdir))
if (xutftowcs_long_path(wdir, dir) < 0 || make_hidden(wdir))
warning("Failed to make '%s' hidden", dir);
git_config_set("core.hideDotFiles",
hide_dotfiles == HIDE_DOTFILES_FALSE ? "false" :
Expand All @@ -309,9 +309,12 @@ void mingw_mark_as_git_dir(const char *dir)
int mingw_mkdir(const char *path, int mode)
{
int ret;
wchar_t wpath[MAX_PATH];
if (xutftowcs_path(wpath, path) < 0)
wchar_t wpath[MAX_LONG_PATH];
/* CreateDirectoryW path limit is 248 (MAX_PATH - 8.3 file name) */
if (xutftowcs_path_ex(wpath, path, MAX_LONG_PATH, -1, 248,
core_long_paths) < 0)
return -1;

ret = _wmkdir(wpath);
if (!ret && hide_dotfiles == HIDE_DOTFILES_TRUE) {
/*
Expand All @@ -331,7 +334,7 @@ int mingw_open (const char *filename, int oflags, ...)
va_list args;
unsigned mode;
int fd;
wchar_t wfilename[MAX_PATH];
wchar_t wfilename[MAX_LONG_PATH];

va_start(args, oflags);
mode = va_arg(args, int);
Expand All @@ -340,7 +343,7 @@ int mingw_open (const char *filename, int oflags, ...)
if (filename && !strcmp(filename, "/dev/null"))
filename = "nul";

if (xutftowcs_path(wfilename, filename) < 0)
if (xutftowcs_long_path(wfilename, filename) < 0)
return -1;
fd = _wopen(wfilename, oflags, mode);

Expand Down Expand Up @@ -410,13 +413,13 @@ FILE *mingw_fopen (const char *filename, const char *otype)
{
int hide = 0;
FILE *file;
wchar_t wfilename[MAX_PATH], wotype[4];
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
if (hide_dotfiles == HIDE_DOTFILES_TRUE &&
basename((char*)filename)[0] == '.')
hide = access(filename, F_OK);
if (filename && !strcmp(filename, "/dev/null"))
filename = "nul";
if (xutftowcs_path(wfilename, filename) < 0 ||
if (xutftowcs_long_path(wfilename, filename) < 0 ||
xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
return NULL;
file = _wfopen(wfilename, wotype);
Expand All @@ -429,13 +432,13 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
{
int hide = 0;
FILE *file;
wchar_t wfilename[MAX_PATH], wotype[4];
wchar_t wfilename[MAX_LONG_PATH], wotype[4];
if (hide_dotfiles == HIDE_DOTFILES_TRUE &&
basename((char*)filename)[0] == '.')
hide = access(filename, F_OK);
if (filename && !strcmp(filename, "/dev/null"))
filename = "nul";
if (xutftowcs_path(wfilename, filename) < 0 ||
if (xutftowcs_long_path(wfilename, filename) < 0 ||
xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
return NULL;
file = _wfreopen(wfilename, wotype, stream);
Expand Down Expand Up @@ -468,25 +471,32 @@ int mingw_fflush(FILE *stream)

int mingw_access(const char *filename, int mode)
{
wchar_t wfilename[MAX_PATH];
if (xutftowcs_path(wfilename, filename) < 0)
wchar_t wfilename[MAX_LONG_PATH];
if (xutftowcs_long_path(wfilename, filename) < 0)
return -1;
/* X_OK is not supported by the MSVCRT version */
return _waccess(wfilename, mode & ~X_OK);
}

/* cached length of current directory for handle_long_path */
static int current_directory_len = 0;

int mingw_chdir(const char *dirname)
{
int result;
wchar_t wdirname[MAX_PATH];
/* SetCurrentDirectoryW doesn't support long paths */
if (xutftowcs_path(wdirname, dirname) < 0)
return -1;
return _wchdir(wdirname);
result = _wchdir(wdirname);
current_directory_len = GetCurrentDirectoryW(0, NULL);
return result;
}

int mingw_chmod(const char *filename, int mode)
{
wchar_t wfilename[MAX_PATH];
if (xutftowcs_path(wfilename, filename) < 0)
wchar_t wfilename[MAX_LONG_PATH];
if (xutftowcs_long_path(wfilename, filename) < 0)
return -1;
return _wchmod(wfilename, mode);
}
Expand All @@ -501,8 +511,8 @@ int mingw_chmod(const char *filename, int mode)
static int do_lstat(int follow, const char *file_name, struct stat *buf)
{
WIN32_FILE_ATTRIBUTE_DATA fdata;
wchar_t wfilename[MAX_PATH];
if (xutftowcs_path(wfilename, file_name) < 0)
wchar_t wfilename[MAX_LONG_PATH];
if (xutftowcs_long_path(wfilename, file_name) < 0)
return -1;

if (GetFileAttributesExW(wfilename, GetFileExInfoStandard, &fdata)) {
Expand Down Expand Up @@ -645,8 +655,8 @@ int mingw_utime (const char *file_name, const struct utimbuf *times)
FILETIME mft, aft;
int fh, rc;
DWORD attrs;
wchar_t wfilename[MAX_PATH];
if (xutftowcs_path(wfilename, file_name) < 0)
wchar_t wfilename[MAX_LONG_PATH];
if (xutftowcs_long_path(wfilename, file_name) < 0)
return -1;

/* must have write permission */
Expand Down Expand Up @@ -694,6 +704,7 @@ unsigned int sleep (unsigned int seconds)
char *mingw_mktemp(char *template)
{
wchar_t wtemplate[MAX_PATH];
/* we need to return the path, thus no long paths here! */
if (xutftowcs_path(wtemplate, template) < 0)
return NULL;
if (!_wmktemp(wtemplate))
Expand Down Expand Up @@ -1136,6 +1147,7 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen
si.hStdOutput = winansi_get_osfhandle(fhout);
si.hStdError = winansi_get_osfhandle(fherr);

/* executables and the current directory don't support long paths */
if (xutftowcs_path(wcmd, cmd) < 0)
return -1;
if (dir && xutftowcs_path(wdir, dir) < 0)
Expand Down Expand Up @@ -1628,8 +1640,9 @@ int mingw_rename(const char *pold, const char *pnew)
{
DWORD attrs, gle;
int tries = 0;
wchar_t wpold[MAX_PATH], wpnew[MAX_PATH];
if (xutftowcs_path(wpold, pold) < 0 || xutftowcs_path(wpnew, pnew) < 0)
wchar_t wpold[MAX_LONG_PATH], wpnew[MAX_LONG_PATH];
if (xutftowcs_long_path(wpold, pold) < 0 ||
xutftowcs_long_path(wpnew, pnew) < 0)
return -1;

/*
Expand Down Expand Up @@ -1905,9 +1918,9 @@ int link(const char *oldpath, const char *newpath)
{
typedef BOOL (WINAPI *T)(LPCWSTR, LPCWSTR, LPSECURITY_ATTRIBUTES);
static T create_hard_link = NULL;
wchar_t woldpath[MAX_PATH], wnewpath[MAX_PATH];
if (xutftowcs_path(woldpath, oldpath) < 0 ||
xutftowcs_path(wnewpath, newpath) < 0)
wchar_t woldpath[MAX_LONG_PATH], wnewpath[MAX_LONG_PATH];
if (xutftowcs_long_path(woldpath, oldpath) < 0 ||
xutftowcs_long_path(wnewpath, newpath) < 0)
return -1;

if (!create_hard_link) {
Expand Down Expand Up @@ -2106,6 +2119,61 @@ int xwcstoutf(char *utf, const wchar_t *wcs, size_t utflen)
return -1;
}

int handle_long_path(wchar_t *path, int len, int max_path, int expand)
{
int result;
wchar_t buf[MAX_LONG_PATH];

/*
* we don't need special handling if path is relative to the current
* directory, and current directory + path don't exceed the desired
* max_path limit. This should cover > 99 % of cases with minimal
* performance impact (git almost always uses relative paths).
*/
if ((len < 2 || (!is_dir_sep(path[0]) && path[1] != ':')) &&
(current_directory_len + len < max_path))
return len;

/*
* handle everything else:
* - absolute paths: "C:\dir\file"
* - absolute UNC paths: "\\server\share\dir\file"
* - absolute paths on current drive: "\dir\file"
* - relative paths on other drive: "X:file"
* - prefixed paths: "\\?\...", "\\.\..."
*/

/* convert to absolute path using GetFullPathNameW */
result = GetFullPathNameW(path, MAX_LONG_PATH, buf, NULL);
if (!result) {
errno = err_win_to_posix(GetLastError());
return -1;
}
/* keep original path if its full path doesn't exceed max_path */
if (result < max_path)
return len;
/* error out if we shouldn't expand the path or buf is too small */
if (!expand || result >= MAX_LONG_PATH - 6) {
errno = ENAMETOOLONG;
return -1;
}

/* prefix full path with "\\?\" or "\\?\UNC\" */
if (buf[0] == '\\') {
/* ...unless already prefixed */
if (buf[1] == '\\' && (buf[2] == '?' || buf[2] == '.'))
return len;

wcscpy(path, L"\\\\?\\UNC\\");
wcscpy(path + 8, buf + 2);
return result + 6;
} else {
wcscpy(path, L"\\\\?\\");
wcscpy(path + 4, buf);
return result + 4;
}
}

/*
* Disable MSVCRT command line wildcard expansion (__getmainargs called from
* mingw startup code, see init.c in mingw runtime).
Expand Down Expand Up @@ -2209,4 +2277,7 @@ void mingw_startup()

/* initialize Unicode console */
winansi_init();

/* init length of current directory for handle_long_path */
current_directory_len = GetCurrentDirectoryW(0, NULL);
}
74 changes: 69 additions & 5 deletions compat/mingw.h
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,39 @@ void mingw_open_html(const char *path);
void mingw_mark_as_git_dir(const char *dir);
#define mark_as_git_dir mingw_mark_as_git_dir

/**
* Max length of long paths (exceeding MAX_PATH). The actual maximum supported
* by NTFS is 32,767 (* sizeof(wchar_t)), but we choose an arbitrary smaller
* value to limit required stack memory.
*/
#define MAX_LONG_PATH 4096

/**
* Handles paths that would exceed the MAX_PATH limit of Windows Unicode APIs.
*
* With expand == false, the function only checks for over-long path and fails
* with ENAMETOOLONG. The path parameter is not modified.
*
* With expand == true, an over-long path is automatically converted in place
* to an absolute path prefixed with '\\?\', and the new length is returned.
* The path parameter must point to a buffer of MAX_LONG_PATH wide characters.
*
* Parameters:
* path: path to check and / or convert
* len: size of path on input (number of wide chars without \0)
* max_path: max short path length to check (usually MAX_PATH = 260, but just
* 248 for CreateDirectoryW)
* expand: false to only check the length, true to expand the path to a
* '\\?\'-prefixed absolute path
*
* Return:
* length of the resulting path, or -1 on failure
*
* Errors:
* ENAMETOOLONG if path is too long
*/
int handle_long_path(wchar_t *path, int len, int max_path, int expand);

/**
* Converts UTF-8 encoded string to UTF-16LE.
*
Expand Down Expand Up @@ -437,18 +470,49 @@ static inline int xutftowcs(wchar_t *wcs, const char *utf, size_t wcslen)
}

/**
* Simplified file system specific variant of xutftowcsn, assumes output
* buffer size is MAX_PATH wide chars and input string is \0-terminated,
* fails with ENAMETOOLONG if input string is too long.
* Simplified file system specific wrapper of xutftowcsn and handle_long_path.
* Converts ERANGE to ENAMETOOLONG. If expand is true, wcs must be at least
* MAX_LONG_PATH wide chars (see handle_long_path).
*/
static inline int xutftowcs_path(wchar_t *wcs, const char *utf)
static inline int xutftowcs_path_ex(wchar_t *wcs, const char *utf,
size_t wcslen, int utflen, int max_path, int expand)
{
int result = xutftowcsn(wcs, utf, MAX_PATH, -1);
int result = xutftowcsn(wcs, utf, wcslen, utflen);
if (result < 0 && errno == ERANGE)
errno = ENAMETOOLONG;
if (result >= 0)
result = handle_long_path(wcs, result, max_path, expand);
return result;
}

/**
* Simplified file system specific variant of xutftowcsn, assumes output
* buffer size is MAX_PATH wide chars and input string is \0-terminated,
* fails with ENAMETOOLONG if input string is too long. Typically used for
* Windows APIs that don't support long paths, e.g. SetCurrentDirectory,
* LoadLibrary, CreateProcess...
*/
static inline int xutftowcs_path(wchar_t *wcs, const char *utf)
{
return xutftowcs_path_ex(wcs, utf, MAX_PATH, -1, MAX_PATH, 0);
}

/* need to re-declare that here as mingw.h is included before cache.h */
extern int core_long_paths;

/**
* Simplified file system specific variant of xutftowcsn for Windows APIs
* that support long paths via '\\?\'-prefix, assumes output buffer size is
* MAX_LONG_PATH wide chars, fails with ENAMETOOLONG if input string is too
* long. The 'core.longpaths' git-config option controls whether the path
* is only checked or expanded to a long path.
*/
static inline int xutftowcs_long_path(wchar_t *wcs, const char *utf)
{
return xutftowcs_path_ex(wcs, utf, MAX_LONG_PATH, -1, MAX_PATH,
core_long_paths);
}

/**
* Converts UTF-16LE encoded string to UTF-8.
*
Expand Down
Loading