Sync with v1.8.5.6
authorJunio C Hamano <gitster@pobox.com>
Wed, 17 Dec 2014 19:20:31 +0000 (11:20 -0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 17 Dec 2014 19:20:31 +0000 (11:20 -0800)
* maint-1.8.5:
Git 1.8.5.6
fsck: complain about NTFS ".git" aliases in trees
read-cache: optionally disallow NTFS .git variants
path: add is_ntfs_dotgit() helper
fsck: complain about HFS+ ".git" aliases in trees
read-cache: optionally disallow HFS+ .git variants
utf8: add is_hfs_dotgit() helper
fsck: notice .git case-insensitively
t1450: refactor ".", "..", and ".git" fsck tests
verify_dotfile(): reject .git case-insensitively
read-tree: add tests for confusing paths like ".." and ".git"
unpack-trees: propagate errors adding entries to the index

13 files changed:
1  2 
Documentation/config.txt
Documentation/git.txt
cache.h
config.c
config.mak.uname
environment.c
fsck.c
path.c
read-cache.c
t/t1450-fsck.sh
t/test-lib.sh
unpack-trees.c
utf8.c
diff --combined Documentation/config.txt
index c26a7c8469826c2650f03e310b8aa360e44c35c6,097fdd47e1aba59f2602bd9b2522a1ef7e7fade4..7076aa928286d6777e6a05c136d8401780379f58
@@@ -78,8 -78,8 +78,8 @@@ be escaped: use `\"` for `"` and `\\` f
  
  The following escape sequences (beside `\"` and `\\`) are recognized:
  `\n` for newline character (NL), `\t` for horizontal tabulation (HT, TAB)
 -and `\b` for backspace (BS).  No other char escape sequence, nor octal
 -char sequences are valid.
 +and `\b` for backspace (BS).  Other char escape sequences (including octal
 +escape sequences) are invalid.
  
  Variable values ending in a `\` are continued on the next line in the
  customary UNIX fashion.
@@@ -234,6 -234,17 +234,17 @@@ core.precomposeunicode:
        When false, file names are handled fully transparent by Git,
        which is backward compatible with older versions of Git.
  
+ core.protectHFS::
+       If set to true, do not allow checkout of paths that would
+       be considered equivalent to `.git` on an HFS+ filesystem.
+       Defaults to `true` on Mac OS, and `false` elsewhere.
+ core.protectNTFS::
+       If set to true, do not allow checkout of paths that would
+       cause problems with the NTFS filesystem, e.g. conflict with
+       8.3 "short" names.
+       Defaults to `true` on Windows, and `false` elsewhere.
  core.trustctime::
        If false, the ctime differences between the index and the
        working tree are ignored; useful when the inode change time
@@@ -567,10 -578,6 +578,10 @@@ be passed to the shell by Git, which wi
  command to `LESS=FRSX less -+S`. The environment tells the command
  to set the `S` option to chop long lines but the command line
  resets it to the default to fold long lines.
 ++
 +Likewise, when the `LV` environment variable is unset, Git sets it
 +to `-c`.  You can override this setting by exporting `LV` with
 +another value or setting `core.pager` to `lv +c`.
  
  core.whitespace::
        A comma separated list of common whitespace problems to
@@@ -827,7 -834,7 +838,7 @@@ color.diff:
        commands will only use color when output is to the terminal.
        Defaults to false.
  +
 -This does not affect linkgit:git-format-patch[1] nor the
 +This does not affect linkgit:git-format-patch[1] or the
  'git-diff-{asterisk}' plumbing commands.  Can be overridden on the
  command line with the `--color[=<when>]` option.
  
@@@ -2030,10 -2037,6 +2041,10 @@@ receive.updateserverinfo:
        If set to true, git-receive-pack will run git-update-server-info
        after receiving data from git-push and updating refs.
  
 +receive.shallowupdate::
 +      If set to true, .git/shallow can be updated when new refs
 +      require new shallow roots. Otherwise those refs are rejected.
 +
  remote.pushdefault::
        The remote to push to by default.  Overrides
        `branch.<name>.remote` for all branches, and is overridden by
@@@ -2095,8 -2098,8 +2106,8 @@@ remote.<name>.vcs:
  
  remote.<name>.prune::
        When set to true, fetching from this remote by default will also
 -      remove any remote-tracking branches which no longer exist on the
 -      remote (as if the `--prune` option was give on the command line).
 +      remove any remote-tracking references that no longer exist on the
 +      remote (as if the `--prune` option was given on the command line).
        Overrides `fetch.prune` settings, if any.
  
  remotes.<group>::
diff --combined Documentation/git.txt
index 3d54378f275958b443a59023d965450542737dfd,2ff62c54eece2e503b7c411d848cf652742093ba..7297fe1ea25af9504b228e88ff450d2daa20a0e9
@@@ -43,18 -43,10 +43,19 @@@ unreleased) version of Git, that is ava
  branch of the `git.git` repository.
  Documentation for older releases are available here:
  
- * link:v1.8.5.5/git.html[documentation for release 1.8.5.5]
 +* link:v1.9.4/git.html[documentation for release 1.9.4]
 +
 +* release notes for
 +  link:RelNotes/1.9.4.txt[1.9.4],
 +  link:RelNotes/1.9.3.txt[1.9.3],
 +  link:RelNotes/1.9.2.txt[1.9.2],
 +  link:RelNotes/1.9.1.txt[1.9.1],
 +  link:RelNotes/1.9.0.txt[1.9.0].
 +
+ * link:v1.8.5.6/git.html[documentation for release 1.8.5.6]
  
  * release notes for
+   link:RelNotes/1.8.5.6.txt[1.8.5.6],
    link:RelNotes/1.8.5.5.txt[1.8.5.5],
    link:RelNotes/1.8.5.4.txt[1.8.5.4],
    link:RelNotes/1.8.5.3.txt[1.8.5.3],
@@@ -819,15 -811,6 +820,15 @@@ temporary file --- it is removed when '
  +
  For a path that is unmerged, 'GIT_EXTERNAL_DIFF' is called with 1
  parameter, <path>.
 ++
 +For each path 'GIT_EXTERNAL_DIFF' is called, two environment variables,
 +'GIT_DIFF_PATH_COUNTER' and 'GIT_DIFF_PATH_TOTAL' are set.
 +
 +'GIT_DIFF_PATH_COUNTER'::
 +      A 1-based counter incremented by one for every path.
 +
 +'GIT_DIFF_PATH_TOTAL'::
 +      The total number of paths.
  
  other
  ~~~~~
diff --combined cache.h
index ebe9a405d811b2cc6b64109db135bb8ca4096e9e,29ed24b80273912d022908abba0ba70ad77cd30c..017c487609eab63d4db54b14591455880d01ebaa
+++ b/cache.h
@@@ -354,7 -354,6 +354,7 @@@ static inline enum object_type object_t
  #define DB_ENVIRONMENT "GIT_OBJECT_DIRECTORY"
  #define INDEX_ENVIRONMENT "GIT_INDEX_FILE"
  #define GRAFT_ENVIRONMENT "GIT_GRAFT_FILE"
 +#define GIT_SHALLOW_FILE_ENVIRONMENT "GIT_SHALLOW_FILE"
  #define TEMPLATE_DIR_ENVIRONMENT "GIT_TEMPLATE_DIR"
  #define CONFIG_ENVIRONMENT "GIT_CONFIG"
  #define CONFIG_DATA_ENVIRONMENT "GIT_CONFIG_PARAMETERS"
@@@ -487,7 -486,7 +487,7 @@@ extern int remove_file_from_index(struc
  #define ADD_CACHE_IMPLICIT_DOT 32     /* internal to "git add -u/-A" */
  extern int add_to_index(struct index_state *, const char *path, struct stat *, int flags);
  extern int add_file_to_index(struct index_state *, const char *path, int flags);
 -extern struct cache_entry *make_cache_entry(unsigned int mode, const unsigned char *sha1, const char *path, int stage, int refresh);
 +extern struct cache_entry *make_cache_entry(unsigned int mode, const unsigned char *sha1, const char *path, int stage, unsigned int refresh_options);
  extern int ce_same_name(const struct cache_entry *a, const struct cache_entry *b);
  extern int index_name_is_other(const struct index_state *, const char *, int);
  extern void *read_blob_data_from_index(struct index_state *, const char *, unsigned long *);
  #define CE_MATCH_RACY_IS_DIRTY                02
  /* do stat comparison even if CE_SKIP_WORKTREE is true */
  #define CE_MATCH_IGNORE_SKIP_WORKTREE 04
 +/* ignore non-existent files during stat update  */
 +#define CE_MATCH_IGNORE_MISSING               0x08
 +/* enable stat refresh */
 +#define CE_MATCH_REFRESH              0x10
  extern int ie_match_stat(const struct index_state *, const struct cache_entry *, struct stat *, unsigned int);
  extern int ie_modified(const struct index_state *, const struct cache_entry *, struct stat *, unsigned int);
  
 -extern int ce_path_match(const struct cache_entry *ce, const struct pathspec *pathspec);
 -
  #define HASH_WRITE_OBJECT 1
  #define HASH_FORMAT_CHECK 2
  extern int index_fd(unsigned char *sha1, int fd, struct stat *st, enum object_type type, const char *path, unsigned flags);
@@@ -587,6 -584,8 +587,8 @@@ extern int fsync_object_files
  extern int core_preload_index;
  extern int core_apply_sparse_checkout;
  extern int precomposed_unicode;
+ extern int protect_hfs;
+ extern int protect_ntfs;
  
  /*
   * The character that begins a commented line in user-editable file
@@@ -739,29 -738,8 +741,29 @@@ enum sharedrepo 
  };
  int git_config_perm(const char *var, const char *value);
  int adjust_shared_perm(const char *path);
 -int safe_create_leading_directories(char *path);
 -int safe_create_leading_directories_const(const char *path);
 +
 +/*
 + * Create the directory containing the named path, using care to be
 + * somewhat safe against races.  Return one of the scld_error values
 + * to indicate success/failure.
 + *
 + * SCLD_VANISHED indicates that one of the ancestor directories of the
 + * path existed at one point during the function call and then
 + * suddenly vanished, probably because another process pruned the
 + * directory while we were working.  To be robust against this kind of
 + * race, callers might want to try invoking the function again when it
 + * returns SCLD_VANISHED.
 + */
 +enum scld_error {
 +      SCLD_OK = 0,
 +      SCLD_FAILED = -1,
 +      SCLD_PERMS = -2,
 +      SCLD_EXISTS = -3,
 +      SCLD_VANISHED = -4
 +};
 +enum scld_error safe_create_leading_directories(char *path);
 +enum scld_error safe_create_leading_directories_const(const char *path);
 +
  int mkdir_in_gitdir(const char *path);
  extern void home_config_paths(char **global, char **xdg, char *file);
  extern char *expand_user_path(const char *path);
@@@ -782,13 -760,14 +784,14 @@@ int longest_ancestor_length(const char 
  char *strip_path_suffix(const char *path, const char *suffix);
  int daemon_avoid_alias(const char *path);
  int offset_1st_component(const char *path);
+ extern int is_ntfs_dotgit(const char *name);
  
  /* object replacement */
 -#define READ_SHA1_FILE_REPLACE 1
 +#define LOOKUP_REPLACE_OBJECT 1
  extern void *read_sha1_file_extended(const unsigned char *sha1, enum object_type *type, unsigned long *size, unsigned flag);
  static inline void *read_sha1_file(const unsigned char *sha1, enum object_type *type, unsigned long *size)
  {
 -      return read_sha1_file_extended(sha1, type, size, READ_SHA1_FILE_REPLACE);
 +      return read_sha1_file_extended(sha1, type, size, LOOKUP_REPLACE_OBJECT);
  }
  extern const unsigned char *do_lookup_replace_object(const unsigned char *sha1);
  static inline const unsigned char *lookup_replace_object(const unsigned char *sha1)
                return sha1;
        return do_lookup_replace_object(sha1);
  }
 +static inline const unsigned char *lookup_replace_object_extended(const unsigned char *sha1, unsigned flag)
 +{
 +      if (!(flag & LOOKUP_REPLACE_OBJECT))
 +              return sha1;
 +      return lookup_replace_object(sha1);
 +}
  
  /* Read and unpack a sha1 file into memory, write memory to a sha1 file */
  extern int sha1_object_info(const unsigned char *, unsigned long *);
@@@ -917,12 -890,9 +920,12 @@@ extern int dwim_log(const char *str, in
  extern int interpret_branch_name(const char *str, int len, struct strbuf *);
  extern int get_sha1_mb(const char *str, unsigned char *sha1);
  
 -extern int refname_match(const char *abbrev_name, const char *full_name, const char **rules);
 -extern const char *ref_rev_parse_rules[];
 -#define ref_fetch_rules ref_rev_parse_rules
 +/*
 + * Return true iff abbrev_name is a possible abbreviation for
 + * full_name according to the rules defined by ref_rev_parse_rules in
 + * refs.c.
 + */
 +extern int refname_match(const char *abbrev_name, const char *full_name);
  
  extern int create_symref(const char *ref, const char *refs_heads_master, const char *logmsg);
  extern int validate_headref(const char *ref);
@@@ -961,7 -931,6 +964,7 @@@ void datestamp(char *buf, int bufsize)
  unsigned long approxidate_careful(const char *, int *);
  unsigned long approxidate_relative(const char *date, const struct timeval *now);
  enum date_mode parse_date_format(const char *format);
 +int date_overflows(unsigned long date);
  
  #define IDENT_STRICT         1
  #define IDENT_NO_DATE        2
@@@ -1108,7 -1077,6 +1111,7 @@@ struct object_info 
        enum object_type *typep;
        unsigned long *sizep;
        unsigned long *disk_sizep;
 +      unsigned char *delta_base_sha1;
  
        /* Response */
        enum {
                } packed;
        } u;
  };
 -extern int sha1_object_info_extended(const unsigned char *, struct object_info *);
 +extern int sha1_object_info_extended(const unsigned char *, struct object_info *, unsigned flags);
  
  /* Dumb servers support */
  extern int update_server_info(int);
@@@ -1271,8 -1239,6 +1274,8 @@@ __attribute__((format (printf, 2, 3))
  extern void trace_argv_printf(const char **argv, const char *format, ...);
  extern void trace_repo_setup(const char *prefix);
  extern int trace_want(const char *key);
 +__attribute__((format (printf, 2, 3)))
 +extern void trace_printf_key(const char *key, const char *fmt, ...);
  extern void trace_strbuf(const char *key, const struct strbuf *buf);
  
  void packet_trace_identity(const char *prog);
diff --combined config.c
index 314d8ee740bea488d8c79d80b3d91530ad15252d,2cd64b6e3a55404a58159ddc34449ff3892c74a6..8984ad20c782795a50e314b5023352612f70f0bc
+++ b/config.c
@@@ -84,12 -84,8 +84,12 @@@ static int handle_path_include(const ch
  {
        int ret = 0;
        struct strbuf buf = STRBUF_INIT;
 -      char *expanded = expand_user_path(path);
 +      char *expanded;
  
 +      if (!path)
 +              return config_error_nonbool("include.path");
 +
 +      expanded = expand_user_path(path);
        if (!expanded)
                return error("Could not expand include path '%s'", path);
        path = expanded;
@@@ -885,6 -881,16 +885,16 @@@ static int git_default_core_config(cons
                return 0;
        }
  
+       if (!strcmp(var, "core.protecthfs")) {
+               protect_hfs = git_config_bool(var, value);
+               return 0;
+       }
+       if (!strcmp(var, "core.protectntfs")) {
+               protect_ntfs = git_config_bool(var, value);
+               return 0;
+       }
        /* Add other config variables here and to Documentation/config.txt. */
        return 0;
  }
@@@ -973,25 -979,25 +983,25 @@@ static int git_default_mailmap_config(c
  
  int git_default_config(const char *var, const char *value, void *dummy)
  {
 -      if (!prefixcmp(var, "core."))
 +      if (starts_with(var, "core."))
                return git_default_core_config(var, value);
  
 -      if (!prefixcmp(var, "user."))
 +      if (starts_with(var, "user."))
                return git_ident_config(var, value, dummy);
  
 -      if (!prefixcmp(var, "i18n."))
 +      if (starts_with(var, "i18n."))
                return git_default_i18n_config(var, value);
  
 -      if (!prefixcmp(var, "branch."))
 +      if (starts_with(var, "branch."))
                return git_default_branch_config(var, value);
  
 -      if (!prefixcmp(var, "push."))
 +      if (starts_with(var, "push."))
                return git_default_push_config(var, value);
  
 -      if (!prefixcmp(var, "mailmap."))
 +      if (starts_with(var, "mailmap."))
                return git_default_mailmap_config(var, value);
  
 -      if (!prefixcmp(var, "advice."))
 +      if (starts_with(var, "advice."))
                return git_default_advice_config(var, value);
  
        if (!strcmp(var, "pager.color") || !strcmp(var, "color.pager")) {
@@@ -1214,14 -1220,15 +1224,14 @@@ int git_config(config_fn_t fn, void *da
   * Find all the stuff for git_config_set() below.
   */
  
 -#define MAX_MATCHES 512
 -
  static struct {
        int baselen;
        char *key;
        int do_not_match;
        regex_t *value_regex;
        int multi_replace;
 -      size_t offset[MAX_MATCHES];
 +      size_t *offset;
 +      unsigned int offset_alloc;
        enum { START, SECTION_SEEN, SECTION_END_SEEN, KEY_SEEN } state;
        int seen;
  } store;
@@@ -1244,11 -1251,11 +1254,11 @@@ static int store_aux(const char *key, c
                if (matches(key, value)) {
                        if (store.seen == 1 && store.multi_replace == 0) {
                                warning("%s has multiple values", key);
 -                      } else if (store.seen >= MAX_MATCHES) {
 -                              error("too many matches for %s", key);
 -                              return 1;
                        }
  
 +                      ALLOC_GROW(store.offset, store.seen + 1,
 +                                 store.offset_alloc);
 +
                        store.offset[store.seen] = cf->do_ftell(cf);
                        store.seen++;
                }
                 * Do not increment matches: this is no match, but we
                 * just made sure we are in the desired section.
                 */
 +              ALLOC_GROW(store.offset, store.seen + 1,
 +                         store.offset_alloc);
                store.offset[store.seen] = cf->do_ftell(cf);
                /* fallthru */
        case SECTION_END_SEEN:
        case START:
                if (matches(key, value)) {
 +                      ALLOC_GROW(store.offset, store.seen + 1,
 +                                 store.offset_alloc);
                        store.offset[store.seen] = cf->do_ftell(cf);
                        store.state = KEY_SEEN;
                        store.seen++;
                        if (strrchr(key, '.') - key == store.baselen &&
                              !strncmp(key, store.key, store.baselen)) {
                                        store.state = SECTION_SEEN;
 +                                      ALLOC_GROW(store.offset,
 +                                                 store.seen + 1,
 +                                                 store.offset_alloc);
                                        store.offset[store.seen] = cf->do_ftell(cf);
                        }
                }
@@@ -1593,7 -1593,6 +1603,7 @@@ int git_config_set_multivar_in_file(con
                        }
                }
  
 +              ALLOC_GROW(store.offset, 1, store.offset_alloc);
                store.offset[0] = 0;
                store.state = START;
                store.seen = 0;
@@@ -1883,7 -1882,7 +1893,7 @@@ int parse_config_key(const char *var
        const char *dot;
  
        /* Does it start with "section." ? */
 -      if (prefixcmp(var, section) || var[section_len] != '.')
 +      if (!starts_with(var, section) || var[section_len] != '.')
                return -1;
  
        /*
diff --combined config.mak.uname
index efaed94d5d69439dc3612345679302577e68a25a,ec7ed7ac3bb74197c8803b63461d519cad43276c..f3cdcbcddf49441fa181e2bcdb60fbe8368db6b2
@@@ -97,6 -97,7 +97,7 @@@ ifeq ($(uname_S),Darwin
        HAVE_DEV_TTY = YesPlease
        COMPAT_OBJS += compat/precompose_utf8.o
        BASIC_CFLAGS += -DPRECOMPOSE_UNICODE
+       BASIC_CFLAGS += -DPROTECT_HFS_DEFAULT=1
  endif
  ifeq ($(uname_S),SunOS)
        NEEDS_SOCKET = YesPlease
@@@ -188,7 -189,6 +189,7 @@@ ifeq ($(uname_S),FreeBSD
        endif
        PYTHON_PATH = /usr/local/bin/python
        HAVE_PATHS_H = YesPlease
 +      GMTIME_UNRELIABLE_ERRORS = UnfortunatelyYes
  endif
  ifeq ($(uname_S),OpenBSD)
        NO_STRCASESTR = YesPlease
        BASIC_LDFLAGS += -L/usr/local/lib
        HAVE_PATHS_H = YesPlease
  endif
 +ifeq ($(uname_S),MirBSD)
 +      NO_STRCASESTR = YesPlease
 +      NO_MEMMEM = YesPlease
 +      USE_ST_TIMESPEC = YesPlease
 +      NEEDS_LIBICONV = YesPlease
 +      HAVE_PATHS_H = YesPlease
 +endif
  ifeq ($(uname_S),NetBSD)
        ifeq ($(shell expr "$(uname_R)" : '[01]\.'),2)
                NEEDS_LIBICONV = YesPlease
@@@ -369,6 -362,7 +370,7 @@@ ifeq ($(uname_S),Windows
        EXTLIBS = user32.lib advapi32.lib shell32.lib wininet.lib ws2_32.lib
        PTHREAD_LIBS =
        lib =
+       BASIC_CFLAGS += -DPROTECT_NTFS_DEFAULT=1
  ifndef DEBUG
        BASIC_CFLAGS += -GL -Os -MT
        BASIC_LDFLAGS += -LTCG
@@@ -513,6 -507,7 +515,7 @@@ ifneq (,$(findstring MINGW,$(uname_S))
        COMPAT_OBJS += compat/mingw.o compat/winansi.o \
                compat/win32/pthread.o compat/win32/syslog.o \
                compat/win32/dirent.o
+       BASIC_CFLAGS += -DPROTECT_NTFS_DEFAULT=1
        BASIC_LDFLAGS += -Wl,--large-address-aware
        EXTLIBS += -lws2_32
        GITLIBS += git.res
diff --combined environment.c
index 4a3437d8a61256029655f25c76256f27a6189248,184748da3ed71ce80fe064598d1167c57d492cdc..39a8c6c3432d93c1cc6d04324d6632032773c30d
@@@ -10,7 -10,6 +10,7 @@@
  #include "cache.h"
  #include "refs.h"
  #include "fmt-merge-msg.h"
 +#include "commit.h"
  
  int trust_executable_bit = 1;
  int trust_ctime = 1;
@@@ -64,6 -63,16 +64,16 @@@ int precomposed_unicode = -1; /* see pr
  struct startup_info *startup_info;
  unsigned long pack_size_limit_cfg;
  
+ #ifndef PROTECT_HFS_DEFAULT
+ #define PROTECT_HFS_DEFAULT 0
+ #endif
+ int protect_hfs = PROTECT_HFS_DEFAULT;
+ #ifndef PROTECT_NTFS_DEFAULT
+ #define PROTECT_NTFS_DEFAULT 0
+ #endif
+ int protect_ntfs = PROTECT_NTFS_DEFAULT;
  /*
   * The character that begins a commented line in user-editable file
   * that is subject to stripspace.
@@@ -98,7 -107,6 +108,7 @@@ const char * const local_repo_env[] = 
        INDEX_ENVIRONMENT,
        NO_REPLACE_OBJECTS_ENVIRONMENT,
        GIT_PREFIX_ENVIRONMENT,
 +      GIT_SHALLOW_FILE_ENVIRONMENT,
        NULL
  };
  
@@@ -126,7 -134,6 +136,7 @@@ static char *expand_namespace(const cha
  static void setup_git_env(void)
  {
        const char *gitfile;
 +      const char *shallow_file;
  
        git_dir = getenv(GIT_DIR_ENVIRONMENT);
        if (!git_dir)
                read_replace_refs = 0;
        namespace = expand_namespace(getenv(GIT_NAMESPACE_ENVIRONMENT));
        namespace_len = strlen(namespace);
 +      shallow_file = getenv(GIT_SHALLOW_FILE_ENVIRONMENT);
 +      if (shallow_file)
 +              set_alternate_shallow_file(shallow_file, 0);
  }
  
  int is_bare_repository(void)
@@@ -177,7 -181,7 +187,7 @@@ const char *get_git_namespace(void
  
  const char *strip_namespace(const char *namespaced_ref)
  {
 -      if (prefixcmp(namespaced_ref, get_git_namespace()) != 0)
 +      if (!starts_with(namespaced_ref, get_git_namespace()))
                return NULL;
        return namespaced_ref + namespace_len;
  }
diff --combined fsck.c
index 64bf279fd7e42da1921a65d725007c52c7ddc5fb,0b76de6f68fa748f789d1405d29da5fb175f80ff..ee7f531e36212a492851565f1bbb56c566039104
--- 1/fsck.c
--- 2/fsck.c
+++ b/fsck.c
@@@ -6,6 -6,7 +6,7 @@@
  #include "commit.h"
  #include "tag.h"
  #include "fsck.h"
+ #include "utf8.h"
  
  static int fsck_walk_tree(struct tree *tree, fsck_walk_func walk, void *data)
  {
@@@ -175,7 -176,8 +176,8 @@@ static int fsck_tree(struct tree *item
                        has_dot = 1;
                if (!strcmp(name, ".."))
                        has_dotdot = 1;
-               if (!strcmp(name, ".git"))
+               if (!strcasecmp(name, ".git") || is_hfs_dotgit(name) ||
+                               is_ntfs_dotgit(name))
                        has_dotgit = 1;
                has_zero_pad |= *(char *)desc.buffer == '0';
                update_tree_entry(&desc);
  
  static int fsck_ident(char **ident, struct object *obj, fsck_error error_func)
  {
 +      char *end;
 +
        if (**ident == '<')
                return error_func(obj, FSCK_ERROR, "invalid author/committer line - missing space before email");
        *ident += strcspn(*ident, "<>\n");
        (*ident)++;
        if (**ident == '0' && (*ident)[1] != ' ')
                return error_func(obj, FSCK_ERROR, "invalid author/committer line - zero-padded date");
 -      *ident += strspn(*ident, "0123456789");
 -      if (**ident != ' ')
 +      if (date_overflows(strtoul(*ident, &end, 10)))
 +              return error_func(obj, FSCK_ERROR, "invalid author/committer line - date causes integer overflow");
 +      if (end == *ident || *end != ' ')
                return error_func(obj, FSCK_ERROR, "invalid author/committer line - bad date");
 -      (*ident)++;
 +      *ident = end + 1;
        if ((**ident != '+' && **ident != '-') ||
            !isdigit((*ident)[1]) ||
            !isdigit((*ident)[2]) ||
@@@ -290,6 -289,9 +292,6 @@@ static int fsck_commit(struct commit *c
        int parents = 0;
        int err;
  
 -      if (commit->date == ULONG_MAX)
 -              return error_func(&commit->object, FSCK_ERROR, "invalid author/committer line");
 -
        if (memcmp(buffer, "tree ", 5))
                return error_func(&commit->object, FSCK_ERROR, "invalid format - expected 'tree' line");
        if (get_sha1_hex(buffer+5, tree_sha1) || buffer[45] != '\n')
diff --combined path.c
index f9c5062427e7d8170a1e2e597fcf22ae517a865e,4ef1b01e05bc1b32d337d13a1b654e4993ea0456..dfd58f4367a2a400c37ceda7192845fd4ce427c1
--- 1/path.c
--- 2/path.c
+++ b/path.c
@@@ -265,12 -265,12 +265,12 @@@ static struct passwd *getpw_str(const c
  char *expand_user_path(const char *path)
  {
        struct strbuf user_path = STRBUF_INIT;
 -      const char *first_slash = strchrnul(path, '/');
        const char *to_copy = path;
  
        if (path == NULL)
                goto return_null;
        if (path[0] == '~') {
 +              const char *first_slash = strchrnul(path, '/');
                const char *username = path + 1;
                size_t username_len = first_slash - username;
                if (username_len == 0) {
@@@ -830,3 -830,36 +830,36 @@@ int offset_1st_component(const char *pa
                return 2 + is_dir_sep(path[2]);
        return is_dir_sep(path[0]);
  }
+ static int only_spaces_and_periods(const char *path, size_t len, size_t skip)
+ {
+       if (len < skip)
+               return 0;
+       len -= skip;
+       path += skip;
+       while (len-- > 0) {
+               char c = *(path++);
+               if (c != ' ' && c != '.')
+                       return 0;
+       }
+       return 1;
+ }
+ int is_ntfs_dotgit(const char *name)
+ {
+       int len;
+       for (len = 0; ; len++)
+               if (!name[len] || name[len] == '\\' || is_dir_sep(name[len])) {
+                       if (only_spaces_and_periods(name, len, 4) &&
+                                       !strncasecmp(name, ".git", 4))
+                               return 1;
+                       if (only_spaces_and_periods(name, len, 5) &&
+                                       !strncasecmp(name, "git~1", 5))
+                               return 1;
+                       if (name[len] != '\\')
+                               return 0;
+                       name += len + 1;
+                       len = -1;
+               }
+ }
diff --combined read-cache.c
index 4b4effd64b842173d6a06656935436515c755242,4fa208b662e0dfd888c8bfa94addaee71244f4f0..ee07cd610ae5efebb0ad2bd233ab380a6d605f83
  #include "resolve-undo.h"
  #include "strbuf.h"
  #include "varint.h"
+ #include "utf8.h"
  
 -static struct cache_entry *refresh_cache_entry(struct cache_entry *ce, int really);
 +static struct cache_entry *refresh_cache_entry(struct cache_entry *ce,
 +                                             unsigned int options);
  
  /* Mask for the name length in ce_flags in the on-disk index */
  
@@@ -697,7 -697,7 +698,7 @@@ int add_file_to_index(struct index_stat
  
  struct cache_entry *make_cache_entry(unsigned int mode,
                const unsigned char *sha1, const char *path, int stage,
 -              int refresh)
 +              unsigned int refresh_options)
  {
        int size, len;
        struct cache_entry *ce;
        ce->ce_namelen = len;
        ce->ce_mode = create_ce_mode(mode);
  
 -      if (refresh)
 -              return refresh_cache_entry(ce, 0);
 -
 -      return ce;
 +      return refresh_cache_entry(ce, refresh_options);
  }
  
  int ce_same_name(const struct cache_entry *a, const struct cache_entry *b)
        return ce_namelen(b) == len && !memcmp(a->name, b->name, len);
  }
  
 -int ce_path_match(const struct cache_entry *ce, const struct pathspec *pathspec)
 -{
 -      return match_pathspec_depth(pathspec, ce->name, ce_namelen(ce), 0, NULL);
 -}
 -
  /*
   * We fundamentally don't like some paths: we don't want
   * dot or dot-dot anywhere, and for obvious reasons don't
@@@ -752,9 -760,10 +753,10 @@@ static int verify_dotfile(const char *r
         * shares the path end test with the ".." case.
         */
        case 'g':
-               if (rest[1] != 'i')
+       case 'G':
+               if (rest[1] != 'i' && rest[1] != 'I')
                        break;
-               if (rest[2] != 't')
+               if (rest[2] != 't' && rest[2] != 'T')
                        break;
                rest += 2;
        /* fallthrough */
@@@ -778,6 -787,10 +780,10 @@@ int verify_path(const char *path
                        return 1;
                if (is_dir_sep(c)) {
  inside:
+                       if (protect_hfs && is_hfs_dotgit(path))
+                               return 0;
+                       if (protect_ntfs && is_ntfs_dotgit(path))
+                               return 0;
                        c = *path++;
                        if ((c == '.' && !verify_dotfile(path)) ||
                            is_dir_sep(c) || c == '\0')
@@@ -1022,12 -1035,10 +1028,12 @@@ static struct cache_entry *refresh_cach
        struct stat st;
        struct cache_entry *updated;
        int changed, size;
 +      int refresh = options & CE_MATCH_REFRESH;
        int ignore_valid = options & CE_MATCH_IGNORE_VALID;
        int ignore_skip_worktree = options & CE_MATCH_IGNORE_SKIP_WORKTREE;
 +      int ignore_missing = options & CE_MATCH_IGNORE_MISSING;
  
 -      if (ce_uptodate(ce))
 +      if (!refresh || ce_uptodate(ce))
                return ce;
  
        /*
        }
  
        if (lstat(ce->name, &st) < 0) {
 +              if (ignore_missing && errno == ENOENT)
 +                      return ce;
                if (err)
                        *err = errno;
                return NULL;
@@@ -1124,9 -1133,7 +1130,9 @@@ int refresh_index(struct index_state *i
        int ignore_submodules = (flags & REFRESH_IGNORE_SUBMODULES) != 0;
        int first = 1;
        int in_porcelain = (flags & REFRESH_IN_PORCELAIN);
 -      unsigned int options = really ? CE_MATCH_IGNORE_VALID : 0;
 +      unsigned int options = (CE_MATCH_REFRESH |
 +                              (really ? CE_MATCH_IGNORE_VALID : 0) |
 +                              (not_new ? CE_MATCH_IGNORE_MISSING : 0));
        const char *modified_fmt;
        const char *deleted_fmt;
        const char *typechange_fmt;
                if (ignore_submodules && S_ISGITLINK(ce->ce_mode))
                        continue;
  
 -              if (pathspec &&
 -                  !match_pathspec_depth(pathspec, ce->name, ce_namelen(ce), 0, seen))
 +              if (pathspec && !ce_path_match(ce, pathspec, seen))
                        filtered = 1;
  
                if (ce_stage(ce)) {
                if (!new) {
                        const char *fmt;
  
 -                      if (not_new && cache_errno == ENOENT)
 -                              continue;
                        if (really && cache_errno == EINVAL) {
                                /* If we are doing --really-refresh that
                                 * means the index is not valid anymore.
        return has_errors;
  }
  
 -static struct cache_entry *refresh_cache_entry(struct cache_entry *ce, int really)
 +static struct cache_entry *refresh_cache_entry(struct cache_entry *ce,
 +                                             unsigned int options)
  {
 -      return refresh_cache_ent(&the_index, ce, really, NULL, NULL);
 +      return refresh_cache_ent(&the_index, ce, options, NULL, NULL);
  }
  
  
diff --combined t/t1450-fsck.sh
index 8c739c96135fe942d7e76236eb34429be93937a8,6edd99a81ef235808ffd36327813ccf1e8832e06..983568a4b9f81d95c5142b02a3760be3644b5bd8
@@@ -142,20 -142,6 +142,20 @@@ test_expect_success '> in name is repor
        grep "error in commit $new" out
  '
  
 +# date is 2^64 + 1
 +test_expect_success 'integer overflow in timestamps is reported' '
 +      git cat-file commit HEAD >basis &&
 +      sed "s/^\\(author .*>\\) [0-9]*/\\1 18446744073709551617/" \
 +              <basis >bad-timestamp &&
 +      new=$(git hash-object -t commit -w --stdin <bad-timestamp) &&
 +      test_when_finished "remove_object $new" &&
 +      git update-ref refs/heads/bogus "$new" &&
 +      test_when_finished "git update-ref -d refs/heads/bogus" &&
 +      git fsck 2>out &&
 +      cat out &&
 +      grep "error in commit $new.*integer overflow" out
 +'
 +
  test_expect_success 'tag pointing to nonexistent' '
        cat >invalid-tag <<-\EOF &&
        object ffffffffffffffffffffffffffffffffffffffff
@@@ -251,35 -237,40 +251,40 @@@ test_expect_success 'fsck notices submo
        )
  '
  
- test_expect_success 'fsck notices "." and ".." in trees' '
-       (
-               git init dots &&
-               cd dots &&
-               blob=$(echo foo | git hash-object -w --stdin) &&
-               tab=$(printf "\\t") &&
-               git mktree <<-EOF &&
-               100644 blob $blob$tab.
-               100644 blob $blob$tab..
-               EOF
-               git fsck 2>out &&
-               cat out &&
-               grep "warning.*\\." out
-       )
- '
- test_expect_success 'fsck notices ".git" in trees' '
-       (
-               git init dotgit &&
-               cd dotgit &&
-               blob=$(echo foo | git hash-object -w --stdin) &&
-               tab=$(printf "\\t") &&
-               git mktree <<-EOF &&
-               100644 blob $blob$tab.git
-               EOF
-               git fsck 2>out &&
-               cat out &&
-               grep "warning.*\\.git" out
-       )
- '
+ while read name path pretty; do
+       while read mode type; do
+               : ${pretty:=$path}
+               test_expect_success "fsck notices $pretty as $type" '
+               (
+                       git init $name-$type &&
+                       cd $name-$type &&
+                       echo content >file &&
+                       git add file &&
+                       git commit -m base &&
+                       blob=$(git rev-parse :file) &&
+                       tree=$(git rev-parse HEAD^{tree}) &&
+                       value=$(eval "echo \$$type") &&
+                       printf "$mode $type %s\t%s" "$value" "$path" >bad &&
+                       bad_tree=$(git mktree <bad) &&
+                       git fsck 2>out &&
+                       cat out &&
+                       grep "warning.*tree $bad_tree" out
+               )'
+       done <<-\EOF
+       100644 blob
+       040000 tree
+       EOF
+ done <<-EOF
+ dot .
+ dotdot ..
+ dotgit .git
+ dotgit-case .GIT
+ dotgit-unicode .gI${u200c}T .gI{u200c}T
+ dotgit-case2 .Git
+ git-tilde1 git~1
+ dotgitdot .git.
+ dot-backslash-case .\\\\.GIT\\\\foobar
+ dotgit-case-backslash .git\\\\foobar
+ EOF
  
  test_done
diff --combined t/test-lib.sh
index 3c7cb1d774cadaa4d625e2d53ba122db4108cb0e,d4569f8df0506f15ec4a836e0363df34d9cbd2ba..afa411e128d9778970b2b2d416605d3c7e86e21a
@@@ -1,4 -1,4 +1,4 @@@
 -#!/bin/sh
 +# Test framework for git.  See t/README for usage.
  #
  # Copyright (c) 2005 Junio C Hamano
  #
@@@ -26,10 -26,6 +26,10 @@@ the
        # outside of t/, e.g. for running tests on the test library
        # itself.
        TEST_DIRECTORY=$(pwd)
 +else
 +      # ensure that TEST_DIRECTORY is an absolute path so that it
 +      # is valid even if the current working directory is changed
 +      TEST_DIRECTORY=$(cd "$TEST_DIRECTORY" && pwd) || exit 1
  fi
  if test -z "$TEST_OUTPUT_DIRECTORY"
  then
@@@ -158,7 -154,11 +158,11 @@@ _z40=0000000000000000000000000000000000
  LF='
  '
  
- export _x05 _x40 _z40 LF
+ # UTF-8 ZERO WIDTH NON-JOINER, which HFS+ ignores
+ # when case-folding filenames
+ u200c=$(printf '\342\200\214')
+ export _x05 _x40 _z40 LF u200c
  
  # Each test should start with something like this, after copyright notices:
  #
@@@ -277,7 -277,7 +281,7 @@@ error "Test script did not set test_des
  
  if test "$help" = "t"
  then
 -      echo "$test_description"
 +      printf '%s\n' "$test_description"
        exit 0
  fi
  
@@@ -328,7 -328,7 +332,7 @@@ test_failure_ () 
        test_failure=$(($test_failure + 1))
        say_color error "not ok $test_count - $1"
        shift
 -      echo "$@" | sed -e 's/^/#       /'
 +      printf '%s\n' "$*" | sed -e 's/^/#      /'
        test "$immediate" = "" || { GIT_EXIT_OK=t; exit 1; }
  }
  
@@@ -481,6 -481,8 +485,6 @@@ test_at_end_hook_ () 
  test_done () {
        GIT_EXIT_OK=t
  
 -      # Note: t0000 relies on $HARNESS_ACTIVE disabling the .counts
 -      # output file
        if test -z "$HARNESS_ACTIVE"
        then
                test_results_dir="$TEST_OUTPUT_DIRECTORY/test-results"
@@@ -575,9 -577,11 +579,9 @@@ the
  
        make_valgrind_symlink () {
                # handle only executables, unless they are shell libraries that
 -              # need to be in the exec-path.  We will just use "#!" as a
 -              # guess for a shell-script, since we have no idea what the user
 -              # may have configured as the shell path.
 +              # need to be in the exec-path.
                test -x "$1" ||
 -              test "#!" = "$(head -c 2 <"$1")" ||
 +              test "# " = "$(head -c 2 <"$1")" ||
                return;
  
                base=$(basename "$1")
@@@ -830,10 -834,6 +834,10 @@@ test_lazy_prereq SYMLINKS 
        ln -s x y && test -h y
  '
  
 +test_lazy_prereq FILEMODE '
 +      test "$(git config --bool core.filemode)" = true
 +'
 +
  test_lazy_prereq CASE_INSENSITIVE_FS '
        echo good >CamelCase &&
        echo bad >camelcase &&
diff --combined unpack-trees.c
index 164354dad7cbbaa7100f73256807680a75188021,648180256e873f01bce91cbd2afb29fb91fe0651..ca7dd0fa5fee1baeb18f5134fced98c933f585b1
@@@ -102,7 -102,7 +102,7 @@@ void setup_unpack_trees_porcelain(struc
                opts->unpack_rejects[i].strdup_strings = 1;
  }
  
- static void do_add_entry(struct unpack_trees_options *o, struct cache_entry *ce,
+ static int do_add_entry(struct unpack_trees_options *o, struct cache_entry *ce,
                         unsigned int set, unsigned int clear)
  {
        clear |= CE_HASHED | CE_UNHASHED;
  
        ce->next = NULL;
        ce->ce_flags = (ce->ce_flags & ~clear) | set;
-       add_index_entry(&o->result, ce,
-                       ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE);
+       return add_index_entry(&o->result, ce,
+                              ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE);
  }
  
  static struct cache_entry *dup_entry(const struct cache_entry *ce)
@@@ -608,7 -608,9 +608,9 @@@ static int unpack_nondirectories(int n
  
        for (i = 0; i < n; i++)
                if (src[i] && src[i] != o->df_conflict_entry)
-                       do_add_entry(o, src[i], 0, 0);
+                       if (do_add_entry(o, src[i], 0, 0))
+                               return -1;
        return 0;
  }
  
@@@ -830,24 -832,23 +832,24 @@@ static int unpack_callback(int n, unsig
  }
  
  static int clear_ce_flags_1(struct cache_entry **cache, int nr,
 -                          char *prefix, int prefix_len,
 +                          struct strbuf *prefix,
                            int select_mask, int clear_mask,
                            struct exclude_list *el, int defval);
  
  /* Whole directory matching */
  static int clear_ce_flags_dir(struct cache_entry **cache, int nr,
 -                            char *prefix, int prefix_len,
 +                            struct strbuf *prefix,
                              char *basename,
                              int select_mask, int clear_mask,
                              struct exclude_list *el, int defval)
  {
        struct cache_entry **cache_end;
        int dtype = DT_DIR;
 -      int ret = is_excluded_from_list(prefix, prefix_len,
 +      int ret = is_excluded_from_list(prefix->buf, prefix->len,
                                        basename, &dtype, el);
 +      int rc;
  
 -      prefix[prefix_len++] = '/';
 +      strbuf_addch(prefix, '/');
  
        /* If undecided, use matching result of parent dir in defval */
        if (ret < 0)
  
        for (cache_end = cache; cache_end != cache + nr; cache_end++) {
                struct cache_entry *ce = *cache_end;
 -              if (strncmp(ce->name, prefix, prefix_len))
 +              if (strncmp(ce->name, prefix->buf, prefix->len))
                        break;
        }
  
         * calling clear_ce_flags_1(). That function will call
         * the expensive is_excluded_from_list() on every entry.
         */
 -      return clear_ce_flags_1(cache, cache_end - cache,
 -                              prefix, prefix_len,
 -                              select_mask, clear_mask,
 -                              el, ret);
 +      rc = clear_ce_flags_1(cache, cache_end - cache,
 +                            prefix,
 +                            select_mask, clear_mask,
 +                            el, ret);
 +      strbuf_setlen(prefix, prefix->len - 1);
 +      return rc;
  }
  
  /*
   * Top level path has prefix_len zero.
   */
  static int clear_ce_flags_1(struct cache_entry **cache, int nr,
 -                          char *prefix, int prefix_len,
 +                          struct strbuf *prefix,
                            int select_mask, int clear_mask,
                            struct exclude_list *el, int defval)
  {
                        continue;
                }
  
 -              if (prefix_len && strncmp(ce->name, prefix, prefix_len))
 +              if (prefix->len && strncmp(ce->name, prefix->buf, prefix->len))
                        break;
  
 -              name = ce->name + prefix_len;
 +              name = ce->name + prefix->len;
                slash = strchr(name, '/');
  
                /* If it's a directory, try whole directory match first */
                        int processed;
  
                        len = slash - name;
 -                      memcpy(prefix + prefix_len, name, len);
 +                      strbuf_add(prefix, name, len);
  
 -                      /*
 -                       * terminate the string (no trailing slash),
 -                       * clear_c_f_dir needs it
 -                       */
 -                      prefix[prefix_len + len] = '\0';
                        processed = clear_ce_flags_dir(cache, cache_end - cache,
 -                                                     prefix, prefix_len + len,
 -                                                     prefix + prefix_len,
 +                                                     prefix,
 +                                                     prefix->buf + prefix->len - len,
                                                       select_mask, clear_mask,
                                                       el, defval);
  
                        /* clear_c_f_dir eats a whole dir already? */
                        if (processed) {
                                cache += processed;
 +                              strbuf_setlen(prefix, prefix->len - len);
                                continue;
                        }
  
 -                      prefix[prefix_len + len++] = '/';
 +                      strbuf_addch(prefix, '/');
                        cache += clear_ce_flags_1(cache, cache_end - cache,
 -                                                prefix, prefix_len + len,
 +                                                prefix,
                                                  select_mask, clear_mask, el, defval);
 +                      strbuf_setlen(prefix, prefix->len - len - 1);
                        continue;
                }
  
@@@ -961,12 -963,9 +963,12 @@@ static int clear_ce_flags(struct cache_
                            int select_mask, int clear_mask,
                            struct exclude_list *el)
  {
 -      char prefix[PATH_MAX];
 +      static struct strbuf prefix = STRBUF_INIT;
 +
 +      strbuf_reset(&prefix);
 +
        return clear_ce_flags_1(cache, nr,
 -                              prefix, 0,
 +                              &prefix,
                                select_mask, clear_mask,
                                el, 0);
  }
diff --combined utf8.c
index 536a9c83e1bbde516df22288d6156160b12a62ce,2c6442cc112dc065fd49d0374af14c6966acb78b..015c8157eda650450f8dfc9bb744959056d10e9b
--- 1/utf8.c
--- 2/utf8.c
+++ b/utf8.c
@@@ -84,10 -84,11 +84,10 @@@ static int git_wcwidth(ucs_char_t ch
         *   "uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c".
         */
        static const struct interval combining[] = {
 -              { 0x0300, 0x0357 }, { 0x035D, 0x036F }, { 0x0483, 0x0486 },
 -              { 0x0488, 0x0489 }, { 0x0591, 0x05A1 }, { 0x05A3, 0x05B9 },
 -              { 0x05BB, 0x05BD }, { 0x05BF, 0x05BF }, { 0x05C1, 0x05C2 },
 -              { 0x05C4, 0x05C4 }, { 0x0600, 0x0603 }, { 0x0610, 0x0615 },
 -              { 0x064B, 0x0658 }, { 0x0670, 0x0670 }, { 0x06D6, 0x06E4 },
 +              { 0x0300, 0x036F }, { 0x0483, 0x0489 }, { 0x0591, 0x05BD },
 +              { 0x05BF, 0x05BF }, { 0x05C1, 0x05C2 }, { 0x05C4, 0x05C5 },
 +              { 0x05C7, 0x05C7 }, { 0x0600, 0x0604 }, { 0x0610, 0x061A },
 +              { 0x064B, 0x065F }, { 0x0670, 0x0670 }, { 0x06D6, 0x06E4 },
                { 0x06E7, 0x06E8 }, { 0x06EA, 0x06ED }, { 0x070F, 0x070F },
                { 0x0711, 0x0711 }, { 0x0730, 0x074A }, { 0x07A6, 0x07B0 },
                { 0x0901, 0x0902 }, { 0x093C, 0x093C }, { 0x0941, 0x0948 },
@@@ -627,3 -628,67 +627,67 @@@ int mbs_chrlen(const char **text, size_
  
        return chrlen;
  }
+ /*
+  * Pick the next char from the stream, folding as an HFS+ filename comparison
+  * would. Note that this is _not_ complete by any means. It's just enough
+  * to make is_hfs_dotgit() work, and should not be used otherwise.
+  */
+ static ucs_char_t next_hfs_char(const char **in)
+ {
+       while (1) {
+               ucs_char_t out = pick_one_utf8_char(in, NULL);
+               /*
+                * check for malformed utf8. Technically this
+                * gets converted to a percent-sequence, but
+                * returning 0 is good enough for is_hfs_dotgit
+                * to realize it cannot be .git
+                */
+               if (!*in)
+                       return 0;
+               /* these code points are ignored completely */
+               switch (out) {
+               case 0x200c: /* ZERO WIDTH NON-JOINER */
+               case 0x200d: /* ZERO WIDTH JOINER */
+               case 0x200e: /* LEFT-TO-RIGHT MARK */
+               case 0x200f: /* RIGHT-TO-LEFT MARK */
+               case 0x202a: /* LEFT-TO-RIGHT EMBEDDING */
+               case 0x202b: /* RIGHT-TO-LEFT EMBEDDING */
+               case 0x202c: /* POP DIRECTIONAL FORMATTING */
+               case 0x202d: /* LEFT-TO-RIGHT OVERRIDE */
+               case 0x202e: /* RIGHT-TO-LEFT OVERRIDE */
+               case 0x206a: /* INHIBIT SYMMETRIC SWAPPING */
+               case 0x206b: /* ACTIVATE SYMMETRIC SWAPPING */
+               case 0x206c: /* INHIBIT ARABIC FORM SHAPING */
+               case 0x206d: /* ACTIVATE ARABIC FORM SHAPING */
+               case 0x206e: /* NATIONAL DIGIT SHAPES */
+               case 0x206f: /* NOMINAL DIGIT SHAPES */
+               case 0xfeff: /* ZERO WIDTH NO-BREAK SPACE */
+                       continue;
+               }
+               /*
+                * there's a great deal of other case-folding that occurs,
+                * but this is enough to catch anything that will convert
+                * to ".git"
+                */
+               return tolower(out);
+       }
+ }
+ int is_hfs_dotgit(const char *path)
+ {
+       ucs_char_t c;
+       if (next_hfs_char(&path) != '.' ||
+           next_hfs_char(&path) != 'g' ||
+           next_hfs_char(&path) != 'i' ||
+           next_hfs_char(&path) != 't')
+               return 0;
+       c = next_hfs_char(&path);
+       if (c && !is_dir_sep(c))
+               return 0;
+       return 1;
+ }