Skip to content

Commit

Permalink
clean: do not traverse mount points
Browse files Browse the repository at this point in the history
It seems to be not exactly rare on Windows to install NTFS junction
points (the equivalent of "bind mounts" on Linux/Unix) in worktrees,
e.g. to map some development tools into a subdirectory.

In such a scenario, it is pretty horrible if `git clean -dfx` traverses
into the mapped directory and starts to "clean up".

Let's just not do that. Let's make sure before we traverse into a
directory that it is not a mount point (or junction).

This addresses #607

Signed-off-by: Johannes Schindelin <[email protected]>
  • Loading branch information
dscho committed Nov 22, 2024
1 parent d84d5d0 commit e376735
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 0 deletions.
14 changes: 14 additions & 0 deletions builtin/clean.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ 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");
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");
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");
static const char *msg_skip_cwd = N_("Refusing to remove current working directory\n");
Expand Down Expand Up @@ -181,6 +183,18 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,
goto out;
}

if (is_mount_point(path)) {
if (!quiet) {
quote_path(path->buf, prefix, &quoted, 0);
printf(dry_run ?
_(msg_would_skip_mount_point) :
_(msg_skip_mount_point), quoted.buf);
}
*dir_gone = 0;

goto out;
}

dir = opendir(path->buf);
if (!dir) {
/* an empty dir could be removed even if it is unreadble */
Expand Down
22 changes: 22 additions & 0 deletions compat/mingw.c
Original file line number Diff line number Diff line change
Expand Up @@ -2685,6 +2685,28 @@ pid_t waitpid(pid_t pid, int *status, int options)
return -1;
}

int mingw_is_mount_point(struct strbuf *path)
{
WIN32_FIND_DATAW findbuf = { 0 };
HANDLE handle;
wchar_t wfilename[MAX_PATH];
int wlen = xutftowcs_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;
Expand Down
3 changes: 3 additions & 0 deletions compat/mingw.h
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,9 @@ static inline void convert_slashes(char *path)
if (*path == '\\')
*path = '/';
}
struct strbuf;
int mingw_is_mount_point(struct strbuf *path);
#define is_mount_point mingw_is_mount_point
#define PATH_SEP ';'
char *mingw_query_user_email(void);
#define query_user_email mingw_query_user_email
Expand Down
4 changes: 4 additions & 0 deletions git-compat-util.h
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,10 @@ static inline int git_has_dir_sep(const char *path)
#define has_dir_sep(path) git_has_dir_sep(path)
#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
Expand Down
39 changes: 39 additions & 0 deletions path.c
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,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;
Expand Down
1 change: 1 addition & 0 deletions path.h
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ int normalize_path_copy(char *dst, const char *src);
int strbuf_normalize_path(struct strbuf *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);

/*
Expand Down
9 changes: 9 additions & 0 deletions t/t7300-clean.sh
Original file line number Diff line number Diff line change
Expand Up @@ -801,4 +801,13 @@ test_expect_success 'traverse into directories that may have ignored entries' '
)
'

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_file target/dont-clean-me
'

test_done

0 comments on commit e376735

Please sign in to comment.