diff --git a/builtin/clean.c b/builtin/clean.c index 8d365720b97014..bcbfec425356ef 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -33,6 +33,10 @@ static const char *msg_remove = N_("Removing %s\n"); static const char *msg_would_remove = N_("Would remove %s\n"); static const char *msg_skip_git_dir = N_("Skipping repository %s\n"); static const char *msg_would_skip_git_dir = N_("Would skip repository %s\n"); +#ifndef CAN_UNLINK_MOUNT_POINTS +static const char *msg_skip_mount_point = N_("Skipping mount point %s\n"); +static const char *msg_would_skip_mount_point = N_("Would skip mount point %s\n"); +#endif static const char *msg_warn_remove_failed = N_("failed to remove %s"); static const char *msg_warn_lstat_failed = N_("could not lstat %s\n"); @@ -169,6 +173,29 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, goto out; } + if (is_mount_point(path)) { +#ifndef CAN_UNLINK_MOUNT_POINTS + if (!quiet) { + quote_path_relative(path->buf, prefix, "ed); + printf(dry_run ? + _(msg_would_skip_mount_point) : + _(msg_skip_mount_point), quoted.buf); + } + *dir_gone = 0; +#else + if (!dry_run && unlink(path->buf)) { + int saved_errno = errno; + quote_path_relative(path->buf, prefix, "ed); + errno = saved_errno; + warning_errno(_(msg_warn_remove_failed), quoted.buf); + *dir_gone = 0; + ret = -1; + } +#endif + + goto out; + } + dir = opendir(path->buf); if (!dir) { /* an empty dir could be removed even if it is unreadble */ @@ -957,6 +984,7 @@ int cmd_clean(int argc, const char **argv, const char *prefix) if (read_cache() < 0) die(_("index file corrupt")); + enable_fscache(active_nr); if (!ignored) setup_standard_excludes(&dir); @@ -1046,6 +1074,7 @@ int cmd_clean(int argc, const char **argv, const char *prefix) strbuf_reset(&abs_path); } + disable_fscache(); strbuf_release(&abs_path); strbuf_release(&buf); string_list_clear(&del_list, 0); diff --git a/cache.h b/cache.h index e74d66390c50a2..f3faf8982304ef 100644 --- a/cache.h +++ b/cache.h @@ -1263,6 +1263,7 @@ int normalize_path_copy_len(char *dst, const char *src, int *prefix_len); int normalize_path_copy(char *dst, const char *src); int longest_ancestor_length(const char *path, struct string_list *prefixes); char *strip_path_suffix(const char *path, const char *suffix); +int is_mount_point_via_stat(struct strbuf *path); int daemon_avoid_alias(const char *path); /* diff --git a/compat/mingw.c b/compat/mingw.c index 89b1c7525b5cd3..05ad32aaab27e4 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -2844,6 +2844,30 @@ pid_t waitpid(pid_t pid, int *status, int options) return -1; } +int (*win32_is_mount_point)(struct strbuf *path) = mingw_is_mount_point; + +int mingw_is_mount_point(struct strbuf *path) +{ + WIN32_FIND_DATAW findbuf = { 0 }; + HANDLE handle; + wchar_t wfilename[MAX_LONG_PATH]; + int wlen = xutftowcs_long_path(wfilename, path->buf); + if (wlen < 0) + die(_("could not get long path for '%s'"), path->buf); + + /* remove trailing slash, if any */ + if (wlen > 0 && wfilename[wlen - 1] == L'/') + wfilename[--wlen] = L'\0'; + + handle = FindFirstFileW(wfilename, &findbuf); + if (handle == INVALID_HANDLE_VALUE) + return 0; + FindClose(handle); + + return (findbuf.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) && + (findbuf.dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT); +} + int xutftowcsn(wchar_t *wcs, const char *utfs, size_t wcslen, int utflen) { int upos = 0, wpos = 0; diff --git a/compat/mingw.h b/compat/mingw.h index 46ebd50245aa49..8d4431c261e9a8 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -463,6 +463,11 @@ static inline void convert_slashes(char *path) if (*path == '\\') *path = '/'; } +struct strbuf; +int mingw_is_mount_point(struct strbuf *path); +extern int (*win32_is_mount_point)(struct strbuf *path); +#define is_mount_point win32_is_mount_point +#define CAN_UNLINK_MOUNT_POINTS 1 #define PATH_SEP ';' extern char *mingw_query_user_email(void); #define query_user_email mingw_query_user_email diff --git a/compat/win32/fscache.c b/compat/win32/fscache.c index 315c7fe916386e..7ec39a1d2047e5 100644 --- a/compat/win32/fscache.c +++ b/compat/win32/fscache.c @@ -41,6 +41,7 @@ static struct trace_key trace_fscache = TRACE_KEY_INIT(FSCACHE); struct fsentry { struct hashmap_entry ent; mode_t st_mode; + ULONG reparse_tag; /* Length of name. */ unsigned short len; /* @@ -180,6 +181,10 @@ static struct fsentry *fseentry_create_entry(struct fscache *cache, struct fsent fse = fsentry_alloc(cache, list, buf, len); + fse->reparse_tag = + fdata->FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT ? + fdata->EaSize : 0; + /* * On certain Windows versions, host directories mapped into * Windows Containers ("Volumes", see https://docs.docker.com/storage/volumes/) @@ -189,8 +194,7 @@ static struct fsentry *fseentry_create_entry(struct fscache *cache, struct fsent * Let's work around this by detecting that situation and * telling Git that these are *not* symbolic links. */ - if (fdata->FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT && - fdata->EaSize == IO_REPARSE_TAG_SYMLINK && + if (fse->reparse_tag == IO_REPARSE_TAG_SYMLINK && sizeof(buf) > (list ? list->len + 1 : 0) + fse->len + 1 && is_inside_windows_container()) { size_t off = 0; @@ -461,6 +465,7 @@ int fscache_enable(size_t initial_size) /* redirect opendir and lstat to the fscache implementations */ opendir = fscache_opendir; lstat = fscache_lstat; + win32_is_mount_point = fscache_is_mount_point; } initialized++; LeaveCriticalSection(&fscache_cs); @@ -521,6 +526,7 @@ void fscache_disable(void) /* reset opendir and lstat to the original implementations */ opendir = dirent_opendir; lstat = mingw_lstat; + win32_is_mount_point = mingw_is_mount_point; } LeaveCriticalSection(&fscache_cs); @@ -588,6 +594,38 @@ int fscache_lstat(const char *filename, struct stat *st) return 0; } +/* + * is_mount_point() replacement, uses cache if enabled, otherwise falls + * back to mingw_is_mount_point(). + */ +int fscache_is_mount_point(struct strbuf *path) +{ + int dirlen, base, len; + struct fsentry key[2], *fse; + struct fscache *cache = fscache_getcache(); + + if (!cache || !do_fscache_enabled(cache, path->buf)) + return mingw_is_mount_point(path); + + cache->lstat_requests++; + /* split path into path + name */ + len = path->len; + if (len && is_dir_sep(path->buf[len - 1])) + len--; + base = len; + while (base && !is_dir_sep(path->buf[base - 1])) + base--; + dirlen = base ? base - 1 : 0; + + /* lookup entry for path + name in cache */ + fsentry_init(key, NULL, path->buf, dirlen); + fsentry_init(key + 1, key, path->buf + base, len - base); + fse = fscache_get(cache, key + 1); + if (!fse) + return mingw_is_mount_point(path); + return fse->reparse_tag == IO_REPARSE_TAG_MOUNT_POINT; +} + typedef struct fscache_DIR { struct DIR base_dir; /* extend base struct DIR */ struct fsentry *pfsentry; diff --git a/compat/win32/fscache.h b/compat/win32/fscache.h index 042b247a542554..386c770a85d321 100644 --- a/compat/win32/fscache.h +++ b/compat/win32/fscache.h @@ -22,6 +22,7 @@ void fscache_flush(void); DIR *fscache_opendir(const char *dir); int fscache_lstat(const char *file_name, struct stat *buf); +int fscache_is_mount_point(struct strbuf *path); /* opaque fscache structure */ struct fscache; diff --git a/git-compat-util.h b/git-compat-util.h index e6f4cb28c53156..bb4000a2b609d0 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -415,6 +415,10 @@ static inline int git_create_symlink(struct index_state *index, const char *targ #define create_symlink git_create_symlink #endif +#ifndef is_mount_point +#define is_mount_point is_mount_point_via_stat +#endif + #ifndef query_user_email #define query_user_email() NULL #endif diff --git a/path.c b/path.c index 96852dbfeb10d0..fd458b1fda4883 100644 --- a/path.c +++ b/path.c @@ -1274,6 +1274,45 @@ char *strip_path_suffix(const char *path, const char *suffix) return offset == -1 ? NULL : xstrndup(path, offset); } +int is_mount_point_via_stat(struct strbuf *path) +{ + size_t len = path->len; + unsigned int current_dev; + struct stat st; + + if (!strcmp("/", path->buf)) + return 1; + + strbuf_addstr(path, "/."); + if (lstat(path->buf, &st)) { + /* + * If we cannot access the current directory, we cannot say + * that it is a bind mount. + */ + strbuf_setlen(path, len); + return 0; + } + current_dev = st.st_dev; + + /* Now look at the parent directory */ + strbuf_addch(path, '.'); + if (lstat(path->buf, &st)) { + /* + * If we cannot access the parent directory, we cannot say + * that it is a bind mount. + */ + strbuf_setlen(path, len); + return 0; + } + strbuf_setlen(path, len); + + /* + * If the device ID differs between current and parent directory, + * then it is a bind mount. + */ + return current_dev != st.st_dev; +} + int daemon_avoid_alias(const char *p) { int sl, ndot; diff --git a/t/t7300-clean.sh b/t/t7300-clean.sh index a2c45d1902ac5d..7868666bb38e06 100755 --- a/t/t7300-clean.sh +++ b/t/t7300-clean.sh @@ -681,4 +681,14 @@ test_expect_success MINGW 'handle clean & core.longpaths = false nicely' ' test_i18ngrep "too long" .git/err ' +test_expect_success MINGW 'clean does not traverse mount points' ' + mkdir target && + >target/dont-clean-me && + git init with-mountpoint && + cmd //c "mklink /j with-mountpoint\\mountpoint target" && + git -C with-mountpoint clean -dfx && + test_path_is_missing with-mountpoint/mountpoint && + test_path_is_file target/dont-clean-me +' + test_done