Windows does not share Unix' convention that files and directories whose names start with a dot are hidden. Hence `.git/`, for example, is in plain view, and caused quite a bit of trouble: some users wanted to peek inside and did not understand what it contains, others modified files. There was a stream of bug reports, until Git for Windows introduced the (opt-out) option to hide at least the .git/ directory by default. The option is configured via the config setting core.hideDotFiles, with the possible values false, true and dotGitOnly (the latter being the default). This is a heavily version of patches we carried in Git for Windows for way too long without submitting them upstream. In this iteration, I also claim authorship for the patch because by now Kusma's changes were so contorted and mutilated beyond recognition by me that I do not want anybody to blame him for my sins. Johannes Schindelin (2): mingw: introduce the 'core.hideDotFiles' setting mingw: remove unnecessary definition Documentation/config.txt | 6 ++++ cache.h | 7 +++++ compat/mingw.c | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ compat/mingw.h | 3 -- config.c | 8 ++++++ environment.c | 1 + t/t0001-init.sh | 30 ++++++++++++++++++++ t/t5611-clone-config.sh | 20 +++++++++++++ 8 files changed, 146 insertions(+), 3 deletions(-) Published-As: https://github.com/dscho/git/releases/tag/hide-dotgit-v2 Interdiff vs v1: diff --git a/Documentation/config.txt b/Documentation/config.txt index 5d4e3b2..8747c2c 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -270,10 +270,10 @@ See linkgit:git-update-index[1]. The default is true (when core.filemode is not specified in the config file). core.hideDotFiles:: - (Windows-only) If true (which is the default), mark newly-created - directories and files whose name starts with a dot as hidden. - If 'dotGitOnly', only the .git/ directory is hidden, but no other - files starting with a dot. + (Windows-only) If true, mark newly-created directories and files whose + name starts with a dot as hidden. If 'dotGitOnly', only the `.git/` + directory is hidden, but no other files starting with a dot. The + default mode is to mark only the `.git/` directory as hidden. core.ignoreCase:: If true, this option enables various workarounds to enable diff --git a/builtin/init-db.c b/builtin/init-db.c index c4269ac..b2d8d40 100644 --- a/builtin/init-db.c +++ b/builtin/init-db.c @@ -370,7 +370,6 @@ int init_db(const char *template_dir, unsigned int flags) check_repository_format(); reinit = create_default_files(template_dir); - mark_as_git_dir(get_git_dir()); create_object_directory(); diff --git a/compat/mingw.c b/compat/mingw.c index 8b8b01c..3ecde84 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -288,31 +288,47 @@ int mingw_rmdir(const char *pathname) static inline int needs_hiding(const char *path) { - return hide_dotfiles == HIDE_DOTFILES_TRUE && - starts_with(basename((char*)path), "."); + const char *basename; + + if (hide_dotfiles == HIDE_DOTFILES_FALSE) + return 0; + + /* We cannot use basename(), as it would remove trailing slashes */ + mingw_skip_dos_drive_prefix((char **)&path); + if (!*path) + return 0; + + for (basename = path; *path; path++) + if (is_dir_sep(*path)) { + do { + path++; + } while (is_dir_sep(*path)); + /* ignore trailing slashes */ + if (*path) + basename = path; + } + + if (hide_dotfiles == HIDE_DOTFILES_TRUE) + return *basename == '.'; + + assert(hide_dotfiles == HIDE_DOTFILES_DOTGITONLY); + return !strncasecmp(".git", basename, 4) && + (!basename[4] || is_dir_sep(basename[4])); } -static int make_hidden(const wchar_t *path) +static int set_hidden_flag(const wchar_t *path, int set) { DWORD attribs = GetFileAttributesW(path); - if (SetFileAttributesW(path, FILE_ATTRIBUTE_HIDDEN | attribs)) + if (set) + attribs |= FILE_ATTRIBUTE_HIDDEN; + else + attribs &= ~FILE_ATTRIBUTE_HIDDEN; + if (SetFileAttributesW(path, attribs)) return 0; errno = err_win_to_posix(GetLastError()); return -1; } -void mingw_mark_as_git_dir(const char *dir) -{ - wchar_t wdir[MAX_PATH]; - if (hide_dotfiles != HIDE_DOTFILES_FALSE && !is_bare_repository()) - if (xutftowcs_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" : - (hide_dotfiles == HIDE_DOTFILES_DOTGITONLY ? - "dotGitOnly" : "true")); -} - int mingw_mkdir(const char *path, int mode) { int ret; @@ -321,7 +337,7 @@ int mingw_mkdir(const char *path, int mode) return -1; ret = _wmkdir(wpath); if (!ret && needs_hiding(path)) - return make_hidden(wpath); + return set_hidden_flag(wpath, 1); return ret; } @@ -348,9 +364,21 @@ int mingw_open (const char *filename, int oflags, ...) if (attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY)) errno = EISDIR; } - if ((oflags & O_CREAT) && fd >= 0 && needs_hiding(filename) && - make_hidden(wfilename)) - warning("Could not mark '%s' as hidden.", filename); + if ((oflags & O_CREAT) && needs_hiding(filename)) { + /* + * Internally, _wopen() uses the CreateFile() API which errors + * out with an ERROR_ACCESS_DENIED if CREATE_ALWAYS was + * specified and an already existing file's attributes do not + * match *exactly*. As there is no mode or flag we can set that + * would correspond to FILE_ATTRIBUTE_HIDDEN, let's just try + * again *without* the O_CREAT flag (that corresponds to the + * CREATE_ALWAYS flag of CreateFile()). + */ + if (fd < 0 && errno == EACCES) + fd = _wopen(wfilename, oflags & ~O_CREAT, mode); + if (fd >= 0 && set_hidden_flag(wfilename, 1)) + warning("Could not mark '%s' as hidden.", filename); + } return fd; } @@ -382,7 +410,7 @@ int mingw_fgetc(FILE *stream) #undef fopen FILE *mingw_fopen (const char *filename, const char *otype) { - int hide = needs_hiding(filename) && access(filename, F_OK); + int hide = needs_hiding(filename); FILE *file; wchar_t wfilename[MAX_PATH], wotype[4]; if (filename && !strcmp(filename, "/dev/null")) @@ -390,15 +418,19 @@ FILE *mingw_fopen (const char *filename, const char *otype) if (xutftowcs_path(wfilename, filename) < 0 || xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0) return NULL; + if (hide && !access(filename, F_OK) && set_hidden_flag(wfilename, 0)) { + error("Could not unhide %s", filename); + return NULL; + } file = _wfopen(wfilename, wotype); - if (file && hide && make_hidden(wfilename)) + if (file && hide && set_hidden_flag(wfilename, 1)) warning("Could not mark '%s' as hidden.", filename); return file; } FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream) { - int hide = needs_hiding(filename) && access(filename, F_OK); + int hide = needs_hiding(filename); FILE *file; wchar_t wfilename[MAX_PATH], wotype[4]; if (filename && !strcmp(filename, "/dev/null")) @@ -406,8 +438,12 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream) if (xutftowcs_path(wfilename, filename) < 0 || xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0) return NULL; + if (hide && !access(filename, F_OK) && set_hidden_flag(wfilename, 0)) { + error("Could not unhide %s", filename); + return NULL; + } file = _wfreopen(wfilename, wotype, stream); - if (file && hide && make_hidden(wfilename)) + if (file && hide && set_hidden_flag(wfilename, 1)) warning("Could not mark '%s' as hidden.", filename); return file; } diff --git a/compat/mingw.h b/compat/mingw.h index 1de70ff..a1808b4 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -416,9 +416,6 @@ int mingw_offset_1st_component(const char *path); void mingw_open_html(const char *path); #define open_html mingw_open_html -void mingw_mark_as_git_dir(const char *dir); -#define mark_as_git_dir mingw_mark_as_git_dir - /** * Converts UTF-8 encoded string to UTF-16LE. * diff --git a/git-compat-util.h b/git-compat-util.h index ea007e4..1f8b5f3 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -1042,8 +1042,4 @@ struct tm *git_gmtime_r(const time_t *, struct tm *); #define getc_unlocked(fh) getc(fh) #endif -#ifndef mark_as_git_dir -#define mark_as_git_dir(x) /* noop */ -#endif - #endif diff --git a/t/t5611-clone-config.sh b/t/t5611-clone-config.sh index 27d730c..e4850b7 100755 --- a/t/t5611-clone-config.sh +++ b/t/t5611-clone-config.sh @@ -37,4 +37,24 @@ test_expect_success 'clone -c config is available during clone' ' test_cmp expect child/file ' +# Tests for the hidden file attribute on windows +is_hidden () { + # Use the output of `attrib`, ignore the absolute path + case "$(attrib "$1")" in *H*?:*) return 0;; esac + return 1 +} + +test_expect_success MINGW 'clone -c core.hideDotFiles' ' + test_commit attributes .gitattributes "" && + rm -rf child && + git clone -c core.hideDotFiles=false . child && + ! is_hidden child/.gitattributes && + rm -rf child && + git clone -c core.hideDotFiles=dotGitOnly . child && + ! is_hidden child/.gitattributes && + rm -rf child && + git clone -c core.hideDotFiles=true . child && + is_hidden child/.gitattributes +' + test_done -- 2.8.2.463.g99156ee -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html