Skip to content

Commit f30afda

Browse files
dschogitster
authored andcommitted
mingw: introduce the 'core.hideDotFiles' setting
On Unix (and Linux), files and directories whose names start with a dot are usually not shown by default. This convention is used by Git: the .git/ directory should be left alone by regular users, and only accessed through Git itself. On Windows, no such convention exists. Instead, there is an explicit flag to mark files or directories as hidden. In the early days, Git for Windows did not mark the .git/ directory (or for that matter, any file or directory whose name starts with a dot) hidden. This lead to quite a bit of confusion, and even loss of data. Consequently, Git for Windows introduced the core.hideDotFiles setting, with three possible values: true, false, and dotGitOnly, defaulting to marking only the .git/ directory as hidden. The rationale: users do not need to access .git/ directly, and indeed (as was demonstrated) should not really see that directory, either. However, not all dot files should be hidden by default, as e.g. Eclipse does not show them (and the user would therefore be unable to see, say, a .gitattributes file). In over five years since the last attempt to bring this patch into core Git, a slightly buggy version of this patch has served Git for Windows' users well: no single report indicated problems with the hidden .git/ directory, and the stream of problems caused by the previously non-hidden .git/ directory simply stopped. The bugs have been fixed during the process of getting this patch upstream. Note that there is a funny quirk we have to pay attention to when creating hidden files: we use Win32's _wopen() function which transmogrifies its arguments and hands off to Win32's CreateFile() function. That latter function errors out with ERROR_ACCESS_DENIED (the equivalent of EACCES) when the equivalent of the O_CREAT flag was passed and the file attributes (including the hidden flag) do not match an existing file's. And _wopen() accepts no parameter that would be transmogrified into said hidden flag. Therefore, we simply try again without O_CREAT. A slightly different method is required for our fopen()/freopen() function as we cannot even *remove* the implicit O_CREAT flag. Therefore, we briefly mark existing files as unhidden when opening them via fopen()/freopen(). The ERROR_ACCESS_DENIED error can also be triggered by opening a file that is marked as a system file (which is unlikely to be tracked in Git), and by trying to create a file that has *just* been deleted and is awaiting the last open handles to be released (which would be handled better by the "Try again?" logic, a story for a different patch series, though). In both cases, it does not matter much if we try again without the O_CREAT flag, read: it does not hurt, either. For details how ERROR_ACCESS_DENIED can be triggered, see https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858 Original-patch-by: Erik Faye-Lund <[email protected]> Initial-Test-By: Pat Thoyts <[email protected]> Signed-off-by: Johannes Schindelin <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 90f7b16 commit f30afda

File tree

7 files changed

+147
-0
lines changed

7 files changed

+147
-0
lines changed

Documentation/config.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,12 @@ See linkgit:git-update-index[1].
269269
+
270270
The default is true (when core.filemode is not specified in the config file).
271271

272+
core.hideDotFiles::
273+
(Windows-only) If true, mark newly-created directories and files whose
274+
name starts with a dot as hidden. If 'dotGitOnly', only the `.git/`
275+
directory is hidden, but no other files starting with a dot. The
276+
default mode is 'dotGitOnly'.
277+
272278
core.ignoreCase::
273279
If true, this option enables various workarounds to enable
274280
Git to work better on filesystems that are not case sensitive,

cache.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,14 @@ extern int ref_paranoia;
698698
extern char comment_line_char;
699699
extern int auto_comment_line_char;
700700

701+
/* Windows only */
702+
enum hide_dotfiles_type {
703+
HIDE_DOTFILES_FALSE = 0,
704+
HIDE_DOTFILES_TRUE,
705+
HIDE_DOTFILES_DOTGITONLY
706+
};
707+
extern enum hide_dotfiles_type hide_dotfiles;
708+
701709
enum branch_track {
702710
BRANCH_TRACK_UNSPECIFIED = -1,
703711
BRANCH_TRACK_NEVER = 0,

compat/mingw.c

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,13 +286,58 @@ int mingw_rmdir(const char *pathname)
286286
return ret;
287287
}
288288

289+
static inline int needs_hiding(const char *path)
290+
{
291+
const char *basename;
292+
293+
if (hide_dotfiles == HIDE_DOTFILES_FALSE)
294+
return 0;
295+
296+
/* We cannot use basename(), as it would remove trailing slashes */
297+
mingw_skip_dos_drive_prefix((char **)&path);
298+
if (!*path)
299+
return 0;
300+
301+
for (basename = path; *path; path++)
302+
if (is_dir_sep(*path)) {
303+
do {
304+
path++;
305+
} while (is_dir_sep(*path));
306+
/* ignore trailing slashes */
307+
if (*path)
308+
basename = path;
309+
}
310+
311+
if (hide_dotfiles == HIDE_DOTFILES_TRUE)
312+
return *basename == '.';
313+
314+
assert(hide_dotfiles == HIDE_DOTFILES_DOTGITONLY);
315+
return !strncasecmp(".git", basename, 4) &&
316+
(!basename[4] || is_dir_sep(basename[4]));
317+
}
318+
319+
static int set_hidden_flag(const wchar_t *path, int set)
320+
{
321+
DWORD original = GetFileAttributesW(path), modified;
322+
if (set)
323+
modified = original | FILE_ATTRIBUTE_HIDDEN;
324+
else
325+
modified = original & ~FILE_ATTRIBUTE_HIDDEN;
326+
if (original == modified || SetFileAttributesW(path, modified))
327+
return 0;
328+
errno = err_win_to_posix(GetLastError());
329+
return -1;
330+
}
331+
289332
int mingw_mkdir(const char *path, int mode)
290333
{
291334
int ret;
292335
wchar_t wpath[MAX_PATH];
293336
if (xutftowcs_path(wpath, path) < 0)
294337
return -1;
295338
ret = _wmkdir(wpath);
339+
if (!ret && needs_hiding(path))
340+
return set_hidden_flag(wpath, 1);
296341
return ret;
297342
}
298343

@@ -319,6 +364,21 @@ int mingw_open (const char *filename, int oflags, ...)
319364
if (attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY))
320365
errno = EISDIR;
321366
}
367+
if ((oflags & O_CREAT) && needs_hiding(filename)) {
368+
/*
369+
* Internally, _wopen() uses the CreateFile() API which errors
370+
* out with an ERROR_ACCESS_DENIED if CREATE_ALWAYS was
371+
* specified and an already existing file's attributes do not
372+
* match *exactly*. As there is no mode or flag we can set that
373+
* would correspond to FILE_ATTRIBUTE_HIDDEN, let's just try
374+
* again *without* the O_CREAT flag (that corresponds to the
375+
* CREATE_ALWAYS flag of CreateFile()).
376+
*/
377+
if (fd < 0 && errno == EACCES)
378+
fd = _wopen(wfilename, oflags & ~O_CREAT, mode);
379+
if (fd >= 0 && set_hidden_flag(wfilename, 1))
380+
warning("could not mark '%s' as hidden.", filename);
381+
}
322382
return fd;
323383
}
324384

@@ -350,27 +410,41 @@ int mingw_fgetc(FILE *stream)
350410
#undef fopen
351411
FILE *mingw_fopen (const char *filename, const char *otype)
352412
{
413+
int hide = needs_hiding(filename);
353414
FILE *file;
354415
wchar_t wfilename[MAX_PATH], wotype[4];
355416
if (filename && !strcmp(filename, "/dev/null"))
356417
filename = "nul";
357418
if (xutftowcs_path(wfilename, filename) < 0 ||
358419
xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
359420
return NULL;
421+
if (hide && !access(filename, F_OK) && set_hidden_flag(wfilename, 0)) {
422+
error("could not unhide %s", filename);
423+
return NULL;
424+
}
360425
file = _wfopen(wfilename, wotype);
426+
if (file && hide && set_hidden_flag(wfilename, 1))
427+
warning("could not mark '%s' as hidden.", filename);
361428
return file;
362429
}
363430

364431
FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
365432
{
433+
int hide = needs_hiding(filename);
366434
FILE *file;
367435
wchar_t wfilename[MAX_PATH], wotype[4];
368436
if (filename && !strcmp(filename, "/dev/null"))
369437
filename = "nul";
370438
if (xutftowcs_path(wfilename, filename) < 0 ||
371439
xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
372440
return NULL;
441+
if (hide && !access(filename, F_OK) && set_hidden_flag(wfilename, 0)) {
442+
error("could not unhide %s", filename);
443+
return NULL;
444+
}
373445
file = _wfreopen(wfilename, wotype, stream);
446+
if (file && hide && set_hidden_flag(wfilename, 1))
447+
warning("could not mark '%s' as hidden.", filename);
374448
return file;
375449
}
376450

config.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,14 @@ static int git_default_core_config(const char *var, const char *value)
912912
return 0;
913913
}
914914

915+
if (!strcmp(var, "core.hidedotfiles")) {
916+
if (value && !strcasecmp(value, "dotgitonly"))
917+
hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
918+
else
919+
hide_dotfiles = git_config_bool(var, value);
920+
return 0;
921+
}
922+
915923
/* Add other config variables here and to Documentation/config.txt. */
916924
return 0;
917925
}

environment.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ int merge_log_config = -1;
6666
int precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */
6767
struct startup_info *startup_info;
6868
unsigned long pack_size_limit_cfg;
69+
enum hide_dotfiles_type hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
6970

7071
#ifndef PROTECT_HFS_DEFAULT
7172
#define PROTECT_HFS_DEFAULT 0

t/t0001-init.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,4 +354,34 @@ test_expect_success SYMLINKS 're-init to move gitdir symlink' '
354354
test_path_is_dir realgitdir/refs
355355
'
356356

357+
# Tests for the hidden file attribute on windows
358+
is_hidden () {
359+
# Use the output of `attrib`, ignore the absolute path
360+
case "$(attrib "$1")" in *H*?:*) return 0;; esac
361+
return 1
362+
}
363+
364+
test_expect_success MINGW '.git hidden' '
365+
rm -rf newdir &&
366+
(
367+
unset GIT_DIR GIT_WORK_TREE
368+
mkdir newdir &&
369+
cd newdir &&
370+
git init &&
371+
is_hidden .git
372+
) &&
373+
check_config newdir/.git false unset
374+
'
375+
376+
test_expect_success MINGW 'bare git dir not hidden' '
377+
rm -rf newdir &&
378+
(
379+
unset GIT_DIR GIT_WORK_TREE GIT_CONFIG
380+
mkdir newdir &&
381+
cd newdir &&
382+
git --bare init
383+
) &&
384+
! is_hidden newdir
385+
'
386+
357387
test_done

t/t5708-clone-config.sh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,24 @@ test_expect_success 'clone -c config is available during clone' '
3737
test_cmp expect child/file
3838
'
3939

40+
# Tests for the hidden file attribute on windows
41+
is_hidden () {
42+
# Use the output of `attrib`, ignore the absolute path
43+
case "$(attrib "$1")" in *H*?:*) return 0;; esac
44+
return 1
45+
}
46+
47+
test_expect_success MINGW 'clone -c core.hideDotFiles' '
48+
test_commit attributes .gitattributes "" &&
49+
rm -rf child &&
50+
git clone -c core.hideDotFiles=false . child &&
51+
! is_hidden child/.gitattributes &&
52+
rm -rf child &&
53+
git clone -c core.hideDotFiles=dotGitOnly . child &&
54+
! is_hidden child/.gitattributes &&
55+
rm -rf child &&
56+
git clone -c core.hideDotFiles=true . child &&
57+
is_hidden child/.gitattributes
58+
'
59+
4060
test_done

0 commit comments

Comments
 (0)