Merge branch 'js/windows-dotgit' into maint
authorJunio C Hamano <gitster@pobox.com>
Thu, 26 May 2016 20:17:23 +0000 (13:17 -0700)
committerJunio C Hamano <gitster@pobox.com>
Thu, 26 May 2016 20:17:23 +0000 (13:17 -0700)
On Windows, .git and optionally any files whose name starts with a
dot are now marked as hidden, with a core.hideDotFiles knob to
customize this behaviour.

* js/windows-dotgit:
mingw: remove unnecessary definition
mingw: introduce the 'core.hideDotFiles' setting

Documentation/config.txt
cache.h
compat/mingw.c
compat/mingw.h
config.c
environment.c
t/t0001-init.sh
t/t5611-clone-config.sh
index 611fe5c0ca223b0f81f275d714a865c57b0d5f68..ce4651be94988394138755920d56e1f3a45cc263 100644 (file)
@@ -279,6 +279,12 @@ 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, 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 'dotGitOnly'.
+
 core.ignoreCase::
        If true, this option enables various workarounds to enable
        Git to work better on filesystems that are not case sensitive,
diff --git a/cache.h b/cache.h
index fd728f079320cb05bb01bb9a5764fcb0943a1f19..4ff196c25926cb1a6662f7582969ed665bae5ee3 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -700,6 +700,14 @@ extern int ref_paranoia;
 extern char comment_line_char;
 extern int auto_comment_line_char;
 
+/* Windows only */
+enum hide_dotfiles_type {
+       HIDE_DOTFILES_FALSE = 0,
+       HIDE_DOTFILES_TRUE,
+       HIDE_DOTFILES_DOTGITONLY
+};
+extern enum hide_dotfiles_type hide_dotfiles;
+
 enum branch_track {
        BRANCH_TRACK_UNSPECIFIED = -1,
        BRANCH_TRACK_NEVER = 0,
index 0413d5c3cdd128ca721c75b588dc557bdbb1f949..a8218e6f0f2177e769cf3841b1c7d7d9b9ed270d 100644 (file)
@@ -286,6 +286,49 @@ int mingw_rmdir(const char *pathname)
        return ret;
 }
 
+static inline int needs_hiding(const 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 set_hidden_flag(const wchar_t *path, int set)
+{
+       DWORD original = GetFileAttributesW(path), modified;
+       if (set)
+               modified = original | FILE_ATTRIBUTE_HIDDEN;
+       else
+               modified = original & ~FILE_ATTRIBUTE_HIDDEN;
+       if (original == modified || SetFileAttributesW(path, modified))
+               return 0;
+       errno = err_win_to_posix(GetLastError());
+       return -1;
+}
+
 int mingw_mkdir(const char *path, int mode)
 {
        int ret;
@@ -293,6 +336,8 @@ int mingw_mkdir(const char *path, int mode)
        if (xutftowcs_path(wpath, path) < 0)
                return -1;
        ret = _wmkdir(wpath);
+       if (!ret && needs_hiding(path))
+               return set_hidden_flag(wpath, 1);
        return ret;
 }
 
@@ -319,6 +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) && 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;
 }
 
@@ -350,6 +410,7 @@ int mingw_fgetc(FILE *stream)
 #undef fopen
 FILE *mingw_fopen (const char *filename, const char *otype)
 {
+       int hide = needs_hiding(filename);
        FILE *file;
        wchar_t wfilename[MAX_PATH], wotype[4];
        if (filename && !strcmp(filename, "/dev/null"))
@@ -357,12 +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 && 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);
        FILE *file;
        wchar_t wfilename[MAX_PATH], wotype[4];
        if (filename && !strcmp(filename, "/dev/null"))
@@ -370,7 +438,13 @@ 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 && set_hidden_flag(wfilename, 1))
+               warning("could not mark '%s' as hidden.", filename);
        return file;
 }
 
index edec9e0253560445db6863ab3ebdd7307e486339..69bb43dc35d4c28724f007d9684715ec40421542 100644 (file)
@@ -417,9 +417,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.
  *
index 4c9b447d76e77cc61e55cfd78ec50878f162a989..adf12a17ef41f70244df320df404d9ccaefd7b4b 100644 (file)
--- a/config.c
+++ b/config.c
@@ -912,6 +912,14 @@ static int git_default_core_config(const char *var, const char *value)
                return 0;
        }
 
+       if (!strcmp(var, "core.hidedotfiles")) {
+               if (value && !strcasecmp(value, "dotgitonly"))
+                       hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
+               else
+                       hide_dotfiles = git_config_bool(var, value);
+               return 0;
+       }
+
        /* Add other config variables here and to Documentation/config.txt. */
        return 0;
 }
index 57acb2fe2aee79a30c4c956dcececd6a7122aeb6..96160a75a53340fcc9b2e1a81d2198d073bd4562 100644 (file)
@@ -63,6 +63,7 @@ int core_apply_sparse_checkout;
 int merge_log_config = -1;
 int precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */
 unsigned long pack_size_limit_cfg;
+enum hide_dotfiles_type hide_dotfiles = HIDE_DOTFILES_DOTGITONLY;
 
 #ifndef PROTECT_HFS_DEFAULT
 #define PROTECT_HFS_DEFAULT 0
index a5b9e7a4c7e85a2f09718ee7618ae6fb48c600fe..a6fdd5ef3a66f06f5b4a787ca8ad8db2ab4da96f 100755 (executable)
@@ -354,4 +354,34 @@ test_expect_success SYMLINKS 're-init to move gitdir symlink' '
        test_path_is_dir realgitdir/refs
 '
 
+# 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 '.git hidden' '
+       rm -rf newdir &&
+       (
+               unset GIT_DIR GIT_WORK_TREE
+               mkdir newdir &&
+               cd newdir &&
+               git init &&
+               is_hidden .git
+       ) &&
+       check_config newdir/.git false unset
+'
+
+test_expect_success MINGW 'bare git dir not hidden' '
+       rm -rf newdir &&
+       (
+               unset GIT_DIR GIT_WORK_TREE GIT_CONFIG
+               mkdir newdir &&
+               cd newdir &&
+               git --bare init
+       ) &&
+       ! is_hidden newdir
+'
+
 test_done
index 27d730c0a7209480c5349e4ace65f92c9e75c699..e4850b778c2f20df02ce187cc8cd057df2759d7d 100755 (executable)
@@ -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