Sync with v2.1.4
authorJunio C Hamano <gitster@pobox.com>
Wed, 17 Dec 2014 19:46:57 +0000 (11:46 -0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 17 Dec 2014 19:46:57 +0000 (11:46 -0800)
* maint-2.1:
Git 2.1.4
Git 2.0.5
Git 1.9.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

1  2 
Documentation/config.txt
Documentation/git.txt
cache.h
config.c
config.mak.uname
fsck.c
path.c
read-cache.c
t/t1450-fsck.sh
t/test-lib.sh
utf8.c
diff --combined Documentation/config.txt
index 922072596fdd80eaaa47746fba5e5591223c5108,0a4d22aaf7ccf57ec67cd342898977e24d2185a8..302d61e76f6684733771e036fc5f1a42ea55f0fa
@@@ -204,26 -204,13 +204,26 @@@ advice.*:
  --
  
  core.fileMode::
 -      If false, the executable bit differences between the index and
 -      the working tree are ignored; useful on broken filesystems like FAT.
 -      See linkgit:git-update-index[1].
 +      Tells Git if the executable bit of files in the working tree
 +      is to be honored.
  +
 -The default is true, except linkgit:git-clone[1] or linkgit:git-init[1]
 -will probe and set core.fileMode false if appropriate when the
 -repository is created.
 +Some filesystems lose the executable bit when a file that is
 +marked as executable is checked out, or checks out an
 +non-executable file with executable bit on.
 +linkgit:git-clone[1] or linkgit:git-init[1] probe the filesystem
 +to see if it handles the executable bit correctly
 +and this variable is automatically set as necessary.
 ++
 +A repository, however, may be on a filesystem that handles
 +the filemode correctly, and this variable is set to 'true'
 +when created, but later may be made accessible from another
 +environment that loses the filemode (e.g. exporting ext4 via
 +CIFS mount, visiting a Cygwin created repository with
 +Git for Windows or Eclipse).
 +In such a case it may be necessary to set this variable to 'false'.
 +See linkgit:git-update-index[1].
 ++
 +The default is true (when core.filemode is not specified in the config file).
  
  core.ignorecase::
        If true, this option enables various workarounds to enable
@@@ -246,6 -233,17 +246,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
@@@ -512,8 -510,7 +523,8 @@@ core.bigFileThreshold:
        Files larger than this size are stored deflated, without
        attempting delta compression.  Storing large files without
        delta compression avoids excessive memory usage, at the
 -      slight expense of increased disk usage.
 +      slight expense of increased disk usage. Additionally files
 +      larger than this size are always treated as binary.
  +
  Default is 512 MiB on all platforms.  This should be reasonable
  for most projects as source code and other text files can still
@@@ -885,11 -882,7 +896,11 @@@ color.grep.<slot>:
  `linenumber`;;
        line number prefix (when using `-n`)
  `match`;;
 -      matching text
 +      matching text (same as setting `matchContext` and `matchSelected`)
 +`matchContext`;;
 +      matching text in context lines
 +`matchSelected`;;
 +      matching text in selected lines
  `selected`;;
        non-matching text in selected lines
  `separator`;;
@@@ -1210,7 -1203,7 +1221,7 @@@ gc.autopacklimit:
        default value is 50.  Setting this to 0 disables it.
  
  gc.autodetach::
 -      Make `git gc --auto` return immediately andrun in background
 +      Make `git gc --auto` return immediately and run in background
        if the system supports it. Default is true.
  
  gc.packrefs::
@@@ -1357,7 -1350,7 +1368,7 @@@ gpg.program:
        same command-line interface as GPG, namely, to verify a detached
        signature, "gpg --verify $file - <$signature" is run, and the
        program is expected to signal a good signature by exiting with
 -      code 0, and to generate an ascii-armored detached signature, the
 +      code 0, and to generate an ASCII-armored detached signature, the
        standard input of "gpg -bsau $key" is fed with the contents to be
        signed, and the program is expected to send the result to its
        standard output.
@@@ -1592,7 -1585,7 +1603,7 @@@ http.useragent:
        Can be overridden by the 'GIT_HTTP_USER_AGENT' environment variable.
  
  http.<url>.*::
 -      Any of the http.* options above can be applied selectively to some urls.
 +      Any of the http.* options above can be applied selectively to some URLs.
        For a config key to match a URL, each element of the config key is
        compared to that of the URL, in the following order:
  +
@@@ -1631,8 -1624,8 +1642,8 @@@ if the URL is `https://user@example.com
  +
  All URLs are normalized before attempting any matching (the password part,
  if embedded in the URL, is always ignored for matching purposes) so that
 -equivalent urls that are simply spelled differently will match properly.
 -Environment variable settings always override any matches.  The urls that are
 +equivalent URLs that are simply spelled differently will match properly.
 +Environment variable settings always override any matches.  The URLs that are
  matched against are those given directly to Git commands.  This means any URLs
  visited as a result of a redirection do not participate in matching.
  
@@@ -1794,12 -1787,6 +1805,12 @@@ mergetool.keepTemporaries:
        preserved, otherwise they will be removed after the tool has
        exited. Defaults to `false`.
  
 +mergetool.writeToTemp::
 +      Git writes temporary 'BASE', 'LOCAL', and 'REMOTE' versions of
 +      conflicting files in the worktree by default.  Git will attempt
 +      to use a temporary directory for these files when set `true`.
 +      Defaults to `false`.
 +
  mergetool.prompt::
        Prompt before each invocation of the merge resolution program.
  
@@@ -1860,11 -1847,10 +1871,11 @@@ pack.depth:
        maximum depth is given on the command line. Defaults to 50.
  
  pack.windowMemory::
 -      The window memory size limit used by linkgit:git-pack-objects[1]
 -      when no limit is given on the command line.  The value can be
 -      suffixed with "k", "m", or "g".  Defaults to 0, meaning no
 -      limit.
 +      The maximum size of memory that is consumed by each thread
 +      in linkgit:git-pack-objects[1] for pack window memory when
 +      no limit is given on the command line.  The value can be
 +      suffixed with "k", "m", or "g".  When left unconfigured (or
 +      set explicitly to 0), there will be no limit.
  
  pack.compression::
        An integer -1..9, indicating the compression level for objects
@@@ -2077,25 -2063,6 +2088,25 @@@ receive.autogc:
        receiving data from git-push and updating refs.  You can stop
        it by setting this variable to false.
  
 +receive.certnonceseed::
 +      By setting this variable to a string, `git receive-pack`
 +      will accept a `git push --signed` and verifies it by using
 +      a "nonce" protected by HMAC using this string as a secret
 +      key.
 +
 +receive.certnonceslop::
 +      When a `git push --signed` sent a push certificate with a
 +      "nonce" that was issued by a receive-pack serving the same
 +      repository within this many seconds, export the "nonce"
 +      found in the certificate to `GIT_PUSH_CERT_NONCE` to the
 +      hooks (instead of what the receive-pack asked the sending
 +      side to include).  This may allow writing checks in
 +      `pre-receive` and `post-receive` a bit easier.  Instead of
 +      checking `GIT_PUSH_CERT_NONCE_SLOP` environment variable
 +      that records by how many seconds the nonce is stale to
 +      decide if they want to accept the certificate, they only
 +      can check `GIT_PUSH_CERT_NONCE_STATUS` is `OK`.
 +
  receive.fsckObjects::
        If it is set to true, git-receive-pack will check all received
        objects. It will abort in the case of a malformed object or a
diff --combined Documentation/git.txt
index afb48d39bbd229bb0a28d2e7e2fb835e63248525,78cc101b03ab3d6b0efcc6ed863e4407b499d1a2..4360a795440b598ed17de3bbcb26b00c18d61654
@@@ -22,7 -22,7 +22,7 @@@ unusually rich command set that provide
  and full access to internals.
  
  See linkgit:gittutorial[7] to get started, then see
 -link:everyday.html[Everyday Git] for a useful minimum set of
 +linkgit:giteveryday[7] for a useful minimum set of
  commands.  The link:user-manual.html[Git User's Manual] has a more
  in-depth introduction.
  
@@@ -43,40 -43,39 +43,44 @@@ unreleased) version of Git, that is ava
  branch of the `git.git` repository.
  Documentation for older releases are available here:
  
- * link:v2.1.3/git.html[documentation for release 2.1.3]
 +* link:v2.2.0/git.html[documentation for release 2.2]
 +
 +* release notes for
 +  link:RelNotes/2.2.0.txt[2.2].
 +
+ * link:v2.1.4/git.html[documentation for release 2.1.4]
  
  * release notes for
+   link:RelNotes/2.1.4.txt[2.1.4],
    link:RelNotes/2.1.3.txt[2.1.3],
    link:RelNotes/2.1.2.txt[2.1.2],
    link:RelNotes/2.1.1.txt[2.1.1],
    link:RelNotes/2.1.0.txt[2.1].
  
- * link:v2.0.4/git.html[documentation for release 2.0.4]
+ * link:v2.0.5/git.html[documentation for release 2.0.5]
  
  * release notes for
+   link:RelNotes/2.0.5.txt[2.0.5],
    link:RelNotes/2.0.4.txt[2.0.4],
    link:RelNotes/2.0.3.txt[2.0.3],
    link:RelNotes/2.0.2.txt[2.0.2],
    link:RelNotes/2.0.1.txt[2.0.1],
    link:RelNotes/2.0.0.txt[2.0.0].
  
- * link:v1.9.4/git.html[documentation for release 1.9.4]
+ * link:v1.9.5/git.html[documentation for release 1.9.5]
  
  * release notes for
+   link:RelNotes/1.9.5.txt[1.9.5],
    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.5/git.html[documentation for release 1.8.5.5]
+ * 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],
@@@ -1104,7 -1103,7 +1108,7 @@@ subscribed to the list to send a messag
  SEE ALSO
  --------
  linkgit:gittutorial[7], linkgit:gittutorial-2[7],
 -link:everyday.html[Everyday Git], linkgit:gitcvs-migration[7],
 +linkgit:giteveryday[7], linkgit:gitcvs-migration[7],
  linkgit:gitglossary[7], linkgit:gitcore-tutorial[7],
  linkgit:gitcli[7], link:user-manual.html[The Git User's Manual],
  linkgit:gitworkflows[7]
diff --combined cache.h
index 99ed096aed03b865dd6e263474e6e6a265681b91,a258d805cda7c155cbe60629f38bd3f323c21d78..22b7b8129072f3905c68f02fe19f463bdb738b9e
+++ b/cache.h
@@@ -8,7 -8,6 +8,7 @@@
  #include "gettext.h"
  #include "convert.h"
  #include "trace.h"
 +#include "string-list.h"
  
  #include SHA1_HEADER
  #ifndef git_SHA_CTX
@@@ -482,7 -481,7 +482,7 @@@ extern int daemonize(void)
                                alloc = (nr); \
                        else \
                                alloc = alloc_nr(alloc); \
 -                      x = xrealloc((x), alloc * sizeof(*(x))); \
 +                      REALLOC_ARRAY(x, alloc); \
                } \
        } while (0)
  
@@@ -570,11 -569,28 +570,11 @@@ extern void fill_stat_cache_info(struc
  #define REFRESH_IN_PORCELAIN  0x0020  /* user friendly output, not "needs update" */
  extern int refresh_index(struct index_state *, unsigned int flags, const struct pathspec *pathspec, char *seen, const char *header_msg);
  
 -struct lock_file {
 -      struct lock_file *next;
 -      int fd;
 -      pid_t owner;
 -      char on_list;
 -      char filename[PATH_MAX];
 -};
 -#define LOCK_DIE_ON_ERROR 1
 -#define LOCK_NODEREF 2
 -extern int unable_to_lock_error(const char *path, int err);
 -extern void unable_to_lock_message(const char *path, int err,
 -                                 struct strbuf *buf);
 -extern NORETURN void unable_to_lock_index_die(const char *path, int err);
 -extern int hold_lock_file_for_update(struct lock_file *, const char *path, int);
 -extern int hold_lock_file_for_append(struct lock_file *, const char *path, int);
 -extern int commit_lock_file(struct lock_file *);
  extern void update_index_if_able(struct index_state *, struct lock_file *);
  
  extern int hold_locked_index(struct lock_file *, int);
  extern void set_alternate_index_output(const char *);
 -extern int close_lock_file(struct lock_file *);
 -extern void rollback_lock_file(struct lock_file *);
 +
  extern int delete_ref(const char *, const unsigned char *sha1, int delopt);
  
  /* Environment bits from configuration mechanism */
@@@ -617,6 -633,8 +617,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
@@@ -831,6 -849,7 +833,7 @@@ int normalize_path_copy(char *dst, cons
  int longest_ancestor_length(const char *path, struct string_list *prefixes);
  char *strip_path_suffix(const char *path, const char *suffix);
  int daemon_avoid_alias(const char *path);
+ extern int is_ntfs_dotgit(const char *name);
  
  /* object replacement */
  #define LOOKUP_REPLACE_OBJECT 1
@@@ -950,8 -969,8 +953,8 @@@ extern int for_each_abbrev(const char *
  extern int get_sha1_hex(const char *hex, unsigned char *sha1);
  
  extern char *sha1_to_hex(const unsigned char *sha1);  /* static buffer result! */
 -extern int read_ref_full(const char *refname, unsigned char *sha1,
 -                       int reading, int *flags);
 +extern int read_ref_full(const char *refname, int resolve_flags,
 +                       unsigned char *sha1, int *flags);
  extern int read_ref(const char *refname, unsigned char *sha1);
  
  /*
   * or the input ref.
   *
   * If the reference cannot be resolved to an object, the behavior
 - * depends on the "reading" argument:
 + * depends on the RESOLVE_REF_READING flag:
   *
 - * - If reading is set, return NULL.
 + * - If RESOLVE_REF_READING is set, return NULL.
   *
 - * - If reading is not set, clear sha1 and return the name of the last
 - *   reference name in the chain, which will either be a non-symbolic
 + * - If RESOLVE_REF_READING is not set, clear sha1 and return the name of
 + *   the last reference name in the chain, which will either be a non-symbolic
   *   reference or an undefined reference.  If this is a prelude to
   *   "writing" to the ref, the return value is the name of the ref
   *   that will actually be created or changed.
   *
 - * If flag is non-NULL, set the value that it points to the
 + * If the RESOLVE_REF_NO_RECURSE flag is passed, only resolves one
 + * level of symbolic reference.  The value stored in sha1 for a symbolic
 + * reference will always be null_sha1 in this case, and the return
 + * value is the reference that the symref refers to directly.
 + *
 + * If flags is non-NULL, set the value that it points to the
   * combination of REF_ISPACKED (if the reference was found among the
 - * packed references) and REF_ISSYMREF (if the initial reference was a
 - * symbolic reference).
 + * packed references), REF_ISSYMREF (if the initial reference was a
 + * symbolic reference), REF_BAD_NAME (if the reference name is ill
 + * formed --- see RESOLVE_REF_ALLOW_BAD_NAME below), and REF_ISBROKEN
 + * (if the ref is malformed or has a bad name). See refs.h for more detail
 + * on each flag.
   *
   * If ref is not a properly-formatted, normalized reference, return
   * NULL.  If more than MAXDEPTH recursive symbolic lookups are needed,
   * give up and return NULL.
   *
 - * errno is set to something meaningful on error.
 + * RESOLVE_REF_ALLOW_BAD_NAME allows resolving refs even when their
 + * name is invalid according to git-check-ref-format(1).  If the name
 + * is bad then the value stored in sha1 will be null_sha1 and the two
 + * flags REF_ISBROKEN and REF_BAD_NAME will be set.
 + *
 + * Even with RESOLVE_REF_ALLOW_BAD_NAME, names that escape the refs/
 + * directory and do not consist of all caps and underscores cannot be
 + * resolved. The function returns NULL for such ref names.
 + * Caps and underscores refers to the special refs, such as HEAD,
 + * FETCH_HEAD and friends, that all live outside of the refs/ directory.
   */
 -extern const char *resolve_ref_unsafe(const char *ref, unsigned char *sha1, int reading, int *flag);
 -extern char *resolve_refdup(const char *ref, unsigned char *sha1, int reading, int *flag);
 +#define RESOLVE_REF_READING 0x01
 +#define RESOLVE_REF_NO_RECURSE 0x02
 +#define RESOLVE_REF_ALLOW_BAD_NAME 0x04
 +extern const char *resolve_ref_unsafe(const char *ref, int resolve_flags, unsigned char *sha1, int *flags);
 +extern char *resolve_refdup(const char *ref, int resolve_flags, unsigned char *sha1, int *flags);
  
  extern int dwim_ref(const char *str, int len, unsigned char *sha1, char **ref);
  extern int dwim_log(const char *str, int len, unsigned char *sha1, char **ref);
@@@ -1041,7 -1040,6 +1044,7 @@@ enum date_mode 
        DATE_SHORT,
        DATE_LOCAL,
        DATE_ISO8601,
 +      DATE_ISO8601_STRICT,
        DATE_RFC2822,
        DATE_RAW
  };
  const char *show_date(unsigned long time, int timezone, enum date_mode mode);
  void show_date_relative(unsigned long time, int tz, const struct timeval *now,
                        struct strbuf *timebuf);
 -int parse_date(const char *date, char *buf, int bufsize);
 +int parse_date(const char *date, struct strbuf *out);
  int parse_date_basic(const char *date, unsigned long *timestamp, int *offset);
  int parse_expiry_date(const char *date, unsigned long *timestamp);
 -void datestamp(char *buf, int bufsize);
 +void datestamp(struct strbuf *out);
  #define approxidate(s) approxidate_careful((s), NULL)
  unsigned long approxidate_careful(const char *, int *);
  unsigned long approxidate_relative(const char *date, const struct timeval *now);
@@@ -1066,7 -1064,6 +1069,7 @@@ extern const char *git_author_info(int)
  extern const char *git_committer_info(int);
  extern const char *fmt_ident(const char *name, const char *email, const char *date_str, int);
  extern const char *fmt_name(const char *name, const char *email);
 +extern const char *ident_default_name(void);
  extern const char *ident_default_email(void);
  extern const char *git_editor(void);
  extern const char *git_pager(int stdout_is_tty);
@@@ -1145,7 -1142,7 +1148,7 @@@ extern void prepare_alt_odb(void)
  extern void read_info_alternates(const char * relative_base, int depth);
  extern void add_to_alternates_file(const char *reference);
  typedef int alt_odb_fn(struct alternate_object_database *, void *);
 -extern void foreach_alt_odb(alt_odb_fn, void*);
 +extern int foreach_alt_odb(alt_odb_fn, void*);
  
  struct pack_window {
        struct pack_window *next;
@@@ -1241,50 -1238,6 +1244,50 @@@ extern unsigned long unpack_object_head
  extern unsigned long get_size_from_delta(struct packed_git *, struct pack_window **, off_t);
  extern int unpack_object_header(struct packed_git *, struct pack_window **, off_t *, unsigned long *);
  
 +/*
 + * Iterate over the files in the loose-object parts of the object
 + * directory "path", triggering the following callbacks:
 + *
 + *  - loose_object is called for each loose object we find.
 + *
 + *  - loose_cruft is called for any files that do not appear to be
 + *    loose objects. Note that we only look in the loose object
 + *    directories "objects/[0-9a-f]{2}/", so we will not report
 + *    "objects/foobar" as cruft.
 + *
 + *  - loose_subdir is called for each top-level hashed subdirectory
 + *    of the object directory (e.g., "$OBJDIR/f0"). It is called
 + *    after the objects in the directory are processed.
 + *
 + * Any callback that is NULL will be ignored. Callbacks returning non-zero
 + * will end the iteration.
 + */
 +typedef int each_loose_object_fn(const unsigned char *sha1,
 +                               const char *path,
 +                               void *data);
 +typedef int each_loose_cruft_fn(const char *basename,
 +                              const char *path,
 +                              void *data);
 +typedef int each_loose_subdir_fn(int nr,
 +                               const char *path,
 +                               void *data);
 +int for_each_loose_file_in_objdir(const char *path,
 +                                each_loose_object_fn obj_cb,
 +                                each_loose_cruft_fn cruft_cb,
 +                                each_loose_subdir_fn subdir_cb,
 +                                void *data);
 +
 +/*
 + * Iterate over loose and packed objects in both the local
 + * repository and any alternates repositories.
 + */
 +typedef int each_packed_object_fn(const unsigned char *sha1,
 +                                struct packed_git *pack,
 +                                uint32_t pos,
 +                                void *data);
 +extern int for_each_loose_object(each_loose_object_fn, void *);
 +extern int for_each_packed_object(each_packed_object_fn, void *);
 +
  struct object_info {
        /* Request */
        enum object_type *typep;
@@@ -1346,7 -1299,7 +1349,7 @@@ extern int git_config_from_buf(config_f
                               const char *buf, size_t len, void *data);
  extern void git_config_push_parameter(const char *text);
  extern int git_config_from_parameters(config_fn_t fn, void *data);
 -extern int git_config(config_fn_t fn, void *);
 +extern void git_config(config_fn_t fn, void *);
  extern int git_config_with_options(config_fn_t fn, void *,
                                   struct git_config_source *config_source,
                                   int respect_includes);
@@@ -1370,7 -1323,6 +1373,7 @@@ extern int git_config_rename_section_in
  extern const char *git_etc_gitconfig(void);
  extern int check_repository_format_version(const char *var, const char *value, void *cb);
  extern int git_env_bool(const char *, int);
 +extern unsigned long git_env_ulong(const char *, unsigned long);
  extern int git_config_system(void);
  extern int config_error_nonbool(const char *);
  #if defined(__GNUC__)
@@@ -1404,69 -1356,6 +1407,69 @@@ extern int parse_config_key(const char 
                            const char **subsection, int *subsection_len,
                            const char **key);
  
 +struct config_set_element {
 +      struct hashmap_entry ent;
 +      char *key;
 +      struct string_list value_list;
 +};
 +
 +struct configset_list_item {
 +      struct config_set_element *e;
 +      int value_index;
 +};
 +
 +/*
 + * the contents of the list are ordered according to their
 + * position in the config files and order of parsing the files.
 + * (i.e. key-value pair at the last position of .git/config will
 + * be at the last item of the list)
 + */
 +struct configset_list {
 +      struct configset_list_item *items;
 +      unsigned int nr, alloc;
 +};
 +
 +struct config_set {
 +      struct hashmap config_hash;
 +      int hash_initialized;
 +      struct configset_list list;
 +};
 +
 +extern void git_configset_init(struct config_set *cs);
 +extern int git_configset_add_file(struct config_set *cs, const char *filename);
 +extern int git_configset_get_value(struct config_set *cs, const char *key, const char **value);
 +extern const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key);
 +extern void git_configset_clear(struct config_set *cs);
 +extern int git_configset_get_string_const(struct config_set *cs, const char *key, const char **dest);
 +extern int git_configset_get_string(struct config_set *cs, const char *key, char **dest);
 +extern int git_configset_get_int(struct config_set *cs, const char *key, int *dest);
 +extern int git_configset_get_ulong(struct config_set *cs, const char *key, unsigned long *dest);
 +extern int git_configset_get_bool(struct config_set *cs, const char *key, int *dest);
 +extern int git_configset_get_bool_or_int(struct config_set *cs, const char *key, int *is_bool, int *dest);
 +extern int git_configset_get_maybe_bool(struct config_set *cs, const char *key, int *dest);
 +extern int git_configset_get_pathname(struct config_set *cs, const char *key, const char **dest);
 +
 +extern int git_config_get_value(const char *key, const char **value);
 +extern const struct string_list *git_config_get_value_multi(const char *key);
 +extern void git_config_clear(void);
 +extern void git_config_iter(config_fn_t fn, void *data);
 +extern int git_config_get_string_const(const char *key, const char **dest);
 +extern int git_config_get_string(const char *key, char **dest);
 +extern int git_config_get_int(const char *key, int *dest);
 +extern int git_config_get_ulong(const char *key, unsigned long *dest);
 +extern int git_config_get_bool(const char *key, int *dest);
 +extern int git_config_get_bool_or_int(const char *key, int *is_bool, int *dest);
 +extern int git_config_get_maybe_bool(const char *key, int *dest);
 +extern int git_config_get_pathname(const char *key, const char **dest);
 +
 +struct key_value_info {
 +      const char *filename;
 +      int linenr;
 +};
 +
 +extern NORETURN void git_die_config(const char *key, const char *err, ...) __attribute__((format(printf, 2, 3)));
 +extern NORETURN void git_die_config_linenr(const char *key, const char *filename, int linenr);
 +
  extern int committer_ident_sufficiently_given(void);
  extern int author_ident_sufficiently_given(void);
  
@@@ -1477,8 -1366,6 +1480,8 @@@ extern const char *git_mailmap_blob
  
  /* IO helper functions */
  extern void maybe_flush_or_die(FILE *, const char *);
 +__attribute__((format (printf, 2, 3)))
 +extern void fprintf_or_die(FILE *, const char *fmt, ...);
  extern int copy_fd(int ifd, int ofd);
  extern int copy_file(const char *dst, const char *src, int mode);
  extern int copy_file_with_time(const char *dst, const char *src, int mode);
diff --combined config.c
index 15a298357796ffa80f7fb2258e55cf95be0b8895,73d8205ae401a8ffc18c37a9bf91d96407f7222d..d5446d2b631e0bc13e78d12fed2c63b0e29aae0a
+++ b/config.c
@@@ -6,12 -6,9 +6,12 @@@
   *
   */
  #include "cache.h"
 +#include "lockfile.h"
  #include "exec_cmd.h"
  #include "strbuf.h"
  #include "quote.h"
 +#include "hashmap.h"
 +#include "string-list.h"
  
  struct config_source {
        struct config_source *prev;
@@@ -40,13 -37,6 +40,13 @@@ static struct config_source *cf
  
  static int zlib_compression_seen;
  
 +/*
 + * Default config_set that contains key-value pairs from the usual set of config
 + * config files (i.e repo specific .git/config, user wide ~/.gitconfig, XDG
 + * config file and the global /etc/gitconfig)
 + */
 +static struct config_set the_config_set;
 +
  static int config_file_fgetc(struct config_source *conf)
  {
        return fgetc(conf->u.file);
@@@ -137,6 -127,7 +137,6 @@@ static int handle_path_include(const ch
  int git_config_include(const char *var, const char *value, void *data)
  {
        struct config_include_data *inc = data;
 -      const char *type;
        int ret;
  
        /*
        if (ret < 0)
                return ret;
  
 -      if (!skip_prefix(var, "include.", &type))
 -              return ret;
 -
 -      if (!strcmp(type, "path"))
 +      if (!strcmp(var, "include.path"))
                ret = handle_path_include(value, inc);
        return ret;
  }
@@@ -243,7 -237,6 +243,7 @@@ static int get_next_char(void
                cf->linenr++;
        if (c == EOF) {
                cf->eof = 1;
 +              cf->linenr++;
                c = '\n';
        }
        return c;
@@@ -319,7 -312,6 +319,7 @@@ static int get_value(config_fn_t fn, vo
  {
        int c;
        char *value;
 +      int ret;
  
        /* Get the full name */
        for (;;) {
                if (!value)
                        return -1;
        }
 -      return fn(name->buf, value, data);
 +      /*
 +       * We already consumed the \n, but we need linenr to point to
 +       * the line we just parsed during the call to fn to get
 +       * accurate line number in error messages.
 +       */
 +      cf->linenr--;
 +      ret = fn(name->buf, value, data);
 +      cf->linenr++;
 +      return ret;
  }
  
  static int get_extended_base_var(struct strbuf *name, int c)
@@@ -466,9 -450,9 +466,9 @@@ static int git_parse_source(config_fn_
                        break;
        }
        if (cf->die_on_error)
 -              die("bad config file line %d in %s", cf->linenr, cf->name);
 +              die(_("bad config file line %d in %s"), cf->linenr, cf->name);
        else
 -              return error("bad config file line %d in %s", cf->linenr, cf->name);
 +              return error(_("bad config file line %d in %s"), cf->linenr, cf->name);
  }
  
  static int parse_unit_factor(const char *end, uintmax_t *val)
@@@ -584,9 -568,9 +584,9 @@@ static void die_bad_number(const char *
                value = "";
  
        if (cf && cf->name)
 -              die("bad numeric config value '%s' for '%s' in %s: %s",
 +              die(_("bad numeric config value '%s' for '%s' in %s: %s"),
                    value, name, cf->name, reason);
 -      die("bad numeric config value '%s' for '%s': %s", value, name, reason);
 +      die(_("bad numeric config value '%s' for '%s': %s"), value, name, reason);
  }
  
  int git_config_int(const char *name, const char *value)
@@@ -671,7 -655,7 +671,7 @@@ int git_config_pathname(const char **de
                return config_error_nonbool(var);
        *dest = expand_user_path(value);
        if (!*dest)
 -              die("Failed to expand user dir in: '%s'", value);
 +              die(_("failed to expand user dir in: '%s'"), value);
        return 0;
  }
  
@@@ -749,7 -733,7 +749,7 @@@ static int git_default_core_config(cons
                if (level == -1)
                        level = Z_DEFAULT_COMPRESSION;
                else if (level < 0 || level > Z_BEST_COMPRESSION)
 -                      die("bad zlib compression level %d", level);
 +                      die(_("bad zlib compression level %d"), level);
                zlib_compression_level = level;
                zlib_compression_seen = 1;
                return 0;
                if (level == -1)
                        level = Z_DEFAULT_COMPRESSION;
                else if (level < 0 || level > Z_BEST_COMPRESSION)
 -                      die("bad zlib compression level %d", level);
 +                      die(_("bad zlib compression level %d"), level);
                core_compression_level = level;
                core_compression_seen = 1;
                if (!zlib_compression_seen)
                else if (!strcmp(value, "link"))
                        object_creation_mode = OBJECT_CREATION_USES_HARDLINKS;
                else
 -                      die("Invalid mode for object creation: %s", value);
 +                      die(_("invalid mode for object creation: %s"), value);
                return 0;
        }
  
                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;
  }
@@@ -1140,28 -1134,12 +1150,28 @@@ const char *git_etc_gitconfig(void
        return system_wide;
  }
  
 +/*
 + * Parse environment variable 'k' as a boolean (in various
 + * possible spellings); if missing, use the default value 'def'.
 + */
  int git_env_bool(const char *k, int def)
  {
        const char *v = getenv(k);
        return v ? git_config_bool(k, v) : def;
  }
  
 +/*
 + * Parse environment variable 'k' as ulong with possibly a unit
 + * suffix; if missing, use the default value 'val'.
 + */
 +unsigned long git_env_ulong(const char *k, unsigned long val)
 +{
 +      const char *v = getenv(k);
 +      if (v && !git_parse_ulong(v, &val))
 +              die("failed to parse %s", k);
 +      return val;
 +}
 +
  int git_config_system(void)
  {
        return !git_env_bool("GIT_CONFIG_NOSYSTEM", 0);
@@@ -1198,7 -1176,7 +1208,7 @@@ int git_config_early(config_fn_t fn, vo
  
        switch (git_config_from_parameters(fn, data)) {
        case -1: /* error */
 -              die("unable to parse command-line config");
 +              die(_("unable to parse command-line config"));
                break;
        case 0: /* found nothing */
                break;
@@@ -1245,365 -1223,9 +1255,365 @@@ int git_config_with_options(config_fn_
        return ret;
  }
  
 -int git_config(config_fn_t fn, void *data)
 +static void git_config_raw(config_fn_t fn, void *data)
 +{
 +      if (git_config_with_options(fn, data, NULL, 1) < 0)
 +              /*
 +               * git_config_with_options() normally returns only
 +               * positive values, as most errors are fatal, and
 +               * non-fatal potential errors are guarded by "if"
 +               * statements that are entered only when no error is
 +               * possible.
 +               *
 +               * If we ever encounter a non-fatal error, it means
 +               * something went really wrong and we should stop
 +               * immediately.
 +               */
 +              die(_("unknown error occured while reading the configuration files"));
 +}
 +
 +static void configset_iter(struct config_set *cs, config_fn_t fn, void *data)
 +{
 +      int i, value_index;
 +      struct string_list *values;
 +      struct config_set_element *entry;
 +      struct configset_list *list = &cs->list;
 +      struct key_value_info *kv_info;
 +
 +      for (i = 0; i < list->nr; i++) {
 +              entry = list->items[i].e;
 +              value_index = list->items[i].value_index;
 +              values = &entry->value_list;
 +              if (fn(entry->key, values->items[value_index].string, data) < 0) {
 +                      kv_info = values->items[value_index].util;
 +                      git_die_config_linenr(entry->key, kv_info->filename, kv_info->linenr);
 +              }
 +      }
 +}
 +
 +static void git_config_check_init(void);
 +
 +void git_config(config_fn_t fn, void *data)
 +{
 +      git_config_check_init();
 +      configset_iter(&the_config_set, fn, data);
 +}
 +
 +static struct config_set_element *configset_find_element(struct config_set *cs, const char *key)
 +{
 +      struct config_set_element k;
 +      struct config_set_element *found_entry;
 +      char *normalized_key;
 +      int ret;
 +      /*
 +       * `key` may come from the user, so normalize it before using it
 +       * for querying entries from the hashmap.
 +       */
 +      ret = git_config_parse_key(key, &normalized_key, NULL);
 +
 +      if (ret)
 +              return NULL;
 +
 +      hashmap_entry_init(&k, strhash(normalized_key));
 +      k.key = normalized_key;
 +      found_entry = hashmap_get(&cs->config_hash, &k, NULL);
 +      free(normalized_key);
 +      return found_entry;
 +}
 +
 +static int configset_add_value(struct config_set *cs, const char *key, const char *value)
 +{
 +      struct config_set_element *e;
 +      struct string_list_item *si;
 +      struct configset_list_item *l_item;
 +      struct key_value_info *kv_info = xmalloc(sizeof(*kv_info));
 +
 +      e = configset_find_element(cs, key);
 +      /*
 +       * Since the keys are being fed by git_config*() callback mechanism, they
 +       * are already normalized. So simply add them without any further munging.
 +       */
 +      if (!e) {
 +              e = xmalloc(sizeof(*e));
 +              hashmap_entry_init(e, strhash(key));
 +              e->key = xstrdup(key);
 +              string_list_init(&e->value_list, 1);
 +              hashmap_add(&cs->config_hash, e);
 +      }
 +      si = string_list_append_nodup(&e->value_list, value ? xstrdup(value) : NULL);
 +
 +      ALLOC_GROW(cs->list.items, cs->list.nr + 1, cs->list.alloc);
 +      l_item = &cs->list.items[cs->list.nr++];
 +      l_item->e = e;
 +      l_item->value_index = e->value_list.nr - 1;
 +
 +      if (cf) {
 +              kv_info->filename = strintern(cf->name);
 +              kv_info->linenr = cf->linenr;
 +      } else {
 +              /* for values read from `git_config_from_parameters()` */
 +              kv_info->filename = NULL;
 +              kv_info->linenr = -1;
 +      }
 +      si->util = kv_info;
 +
 +      return 0;
 +}
 +
 +static int config_set_element_cmp(const struct config_set_element *e1,
 +                               const struct config_set_element *e2, const void *unused)
 +{
 +      return strcmp(e1->key, e2->key);
 +}
 +
 +void git_configset_init(struct config_set *cs)
 +{
 +      hashmap_init(&cs->config_hash, (hashmap_cmp_fn)config_set_element_cmp, 0);
 +      cs->hash_initialized = 1;
 +      cs->list.nr = 0;
 +      cs->list.alloc = 0;
 +      cs->list.items = NULL;
 +}
 +
 +void git_configset_clear(struct config_set *cs)
 +{
 +      struct config_set_element *entry;
 +      struct hashmap_iter iter;
 +      if (!cs->hash_initialized)
 +              return;
 +
 +      hashmap_iter_init(&cs->config_hash, &iter);
 +      while ((entry = hashmap_iter_next(&iter))) {
 +              free(entry->key);
 +              string_list_clear(&entry->value_list, 1);
 +      }
 +      hashmap_free(&cs->config_hash, 1);
 +      cs->hash_initialized = 0;
 +      free(cs->list.items);
 +      cs->list.nr = 0;
 +      cs->list.alloc = 0;
 +      cs->list.items = NULL;
 +}
 +
 +static int config_set_callback(const char *key, const char *value, void *cb)
 +{
 +      struct config_set *cs = cb;
 +      configset_add_value(cs, key, value);
 +      return 0;
 +}
 +
 +int git_configset_add_file(struct config_set *cs, const char *filename)
 +{
 +      return git_config_from_file(config_set_callback, filename, cs);
 +}
 +
 +int git_configset_get_value(struct config_set *cs, const char *key, const char **value)
 +{
 +      const struct string_list *values = NULL;
 +      /*
 +       * Follows "last one wins" semantic, i.e., if there are multiple matches for the
 +       * queried key in the files of the configset, the value returned will be the last
 +       * value in the value list for that key.
 +       */
 +      values = git_configset_get_value_multi(cs, key);
 +
 +      if (!values)
 +              return 1;
 +      assert(values->nr > 0);
 +      *value = values->items[values->nr - 1].string;
 +      return 0;
 +}
 +
 +const struct string_list *git_configset_get_value_multi(struct config_set *cs, const char *key)
  {
 -      return git_config_with_options(fn, data, NULL, 1);
 +      struct config_set_element *e = configset_find_element(cs, key);
 +      return e ? &e->value_list : NULL;
 +}
 +
 +int git_configset_get_string_const(struct config_set *cs, const char *key, const char **dest)
 +{
 +      const char *value;
 +      if (!git_configset_get_value(cs, key, &value))
 +              return git_config_string(dest, key, value);
 +      else
 +              return 1;
 +}
 +
 +int git_configset_get_string(struct config_set *cs, const char *key, char **dest)
 +{
 +      return git_configset_get_string_const(cs, key, (const char **)dest);
 +}
 +
 +int git_configset_get_int(struct config_set *cs, const char *key, int *dest)
 +{
 +      const char *value;
 +      if (!git_configset_get_value(cs, key, &value)) {
 +              *dest = git_config_int(key, value);
 +              return 0;
 +      } else
 +              return 1;
 +}
 +
 +int git_configset_get_ulong(struct config_set *cs, const char *key, unsigned long *dest)
 +{
 +      const char *value;
 +      if (!git_configset_get_value(cs, key, &value)) {
 +              *dest = git_config_ulong(key, value);
 +              return 0;
 +      } else
 +              return 1;
 +}
 +
 +int git_configset_get_bool(struct config_set *cs, const char *key, int *dest)
 +{
 +      const char *value;
 +      if (!git_configset_get_value(cs, key, &value)) {
 +              *dest = git_config_bool(key, value);
 +              return 0;
 +      } else
 +              return 1;
 +}
 +
 +int git_configset_get_bool_or_int(struct config_set *cs, const char *key,
 +                              int *is_bool, int *dest)
 +{
 +      const char *value;
 +      if (!git_configset_get_value(cs, key, &value)) {
 +              *dest = git_config_bool_or_int(key, value, is_bool);
 +              return 0;
 +      } else
 +              return 1;
 +}
 +
 +int git_configset_get_maybe_bool(struct config_set *cs, const char *key, int *dest)
 +{
 +      const char *value;
 +      if (!git_configset_get_value(cs, key, &value)) {
 +              *dest = git_config_maybe_bool(key, value);
 +              if (*dest == -1)
 +                      return -1;
 +              return 0;
 +      } else
 +              return 1;
 +}
 +
 +int git_configset_get_pathname(struct config_set *cs, const char *key, const char **dest)
 +{
 +      const char *value;
 +      if (!git_configset_get_value(cs, key, &value))
 +              return git_config_pathname(dest, key, value);
 +      else
 +              return 1;
 +}
 +
 +static void git_config_check_init(void)
 +{
 +      if (the_config_set.hash_initialized)
 +              return;
 +      git_configset_init(&the_config_set);
 +      git_config_raw(config_set_callback, &the_config_set);
 +}
 +
 +void git_config_clear(void)
 +{
 +      if (!the_config_set.hash_initialized)
 +              return;
 +      git_configset_clear(&the_config_set);
 +}
 +
 +int git_config_get_value(const char *key, const char **value)
 +{
 +      git_config_check_init();
 +      return git_configset_get_value(&the_config_set, key, value);
 +}
 +
 +const struct string_list *git_config_get_value_multi(const char *key)
 +{
 +      git_config_check_init();
 +      return git_configset_get_value_multi(&the_config_set, key);
 +}
 +
 +int git_config_get_string_const(const char *key, const char **dest)
 +{
 +      int ret;
 +      git_config_check_init();
 +      ret = git_configset_get_string_const(&the_config_set, key, dest);
 +      if (ret < 0)
 +              git_die_config(key, NULL);
 +      return ret;
 +}
 +
 +int git_config_get_string(const char *key, char **dest)
 +{
 +      git_config_check_init();
 +      return git_config_get_string_const(key, (const char **)dest);
 +}
 +
 +int git_config_get_int(const char *key, int *dest)
 +{
 +      git_config_check_init();
 +      return git_configset_get_int(&the_config_set, key, dest);
 +}
 +
 +int git_config_get_ulong(const char *key, unsigned long *dest)
 +{
 +      git_config_check_init();
 +      return git_configset_get_ulong(&the_config_set, key, dest);
 +}
 +
 +int git_config_get_bool(const char *key, int *dest)
 +{
 +      git_config_check_init();
 +      return git_configset_get_bool(&the_config_set, key, dest);
 +}
 +
 +int git_config_get_bool_or_int(const char *key, int *is_bool, int *dest)
 +{
 +      git_config_check_init();
 +      return git_configset_get_bool_or_int(&the_config_set, key, is_bool, dest);
 +}
 +
 +int git_config_get_maybe_bool(const char *key, int *dest)
 +{
 +      git_config_check_init();
 +      return git_configset_get_maybe_bool(&the_config_set, key, dest);
 +}
 +
 +int git_config_get_pathname(const char *key, const char **dest)
 +{
 +      int ret;
 +      git_config_check_init();
 +      ret = git_configset_get_pathname(&the_config_set, key, dest);
 +      if (ret < 0)
 +              git_die_config(key, NULL);
 +      return ret;
 +}
 +
 +NORETURN
 +void git_die_config_linenr(const char *key, const char *filename, int linenr)
 +{
 +      if (!filename)
 +              die(_("unable to parse '%s' from command-line config"), key);
 +      else
 +              die(_("bad config variable '%s' in file '%s' at line %d"),
 +                  key, filename, linenr);
 +}
 +
 +NORETURN __attribute__((format(printf, 2, 3)))
 +void git_die_config(const char *key, const char *err, ...)
 +{
 +      const struct string_list *values;
 +      struct key_value_info *kv_info;
 +
 +      if (err) {
 +              va_list params;
 +              va_start(params, err);
 +              vreportf("error: ", err, params);
 +              va_end(params);
 +      }
 +      values = git_config_get_value_multi(key);
 +      kv_info = values->items[values->nr - 1].util;
 +      git_die_config_linenr(key, kv_info->filename, kv_info->linenr);
  }
  
  /*
@@@ -1644,7 -1266,7 +1654,7 @@@ static int store_aux(const char *key, c
        case KEY_SEEN:
                if (matches(key, value)) {
                        if (store.seen == 1 && store.multi_replace == 0) {
 -                              warning("%s has multiple values", key);
 +                              warning(_("%s has multiple values"), key);
                        }
  
                        ALLOC_GROW(store.offset, store.seen + 1,
@@@ -2041,9 -1663,9 +2051,9 @@@ int git_config_set_multivar_in_file(con
                        MAP_PRIVATE, in_fd, 0);
                close(in_fd);
  
 -              if (chmod(lock->filename, st.st_mode & 07777) < 0) {
 +              if (chmod(lock->filename.buf, st.st_mode & 07777) < 0) {
                        error("chmod on %s failed: %s",
 -                              lock->filename, strerror(errno));
 +                              lock->filename.buf, strerror(errno));
                        ret = CONFIG_NO_WRITE;
                        goto out_free;
                }
        if (commit_lock_file(lock) < 0) {
                error("could not commit config file %s", config_filename);
                ret = CONFIG_NO_WRITE;
 +              lock = NULL;
                goto out_free;
        }
  
        lock = NULL;
        ret = 0;
  
 +      /* Invalidate the config cache */
 +      git_config_clear();
 +
  out_free:
        if (lock)
                rollback_lock_file(lock);
        return ret;
  
  write_err_out:
 -      ret = write_error(lock->filename);
 +      ret = write_error(lock->filename.buf);
        goto out_free;
  
  }
@@@ -2224,9 -1842,9 +2234,9 @@@ int git_config_rename_section_in_file(c
  
        fstat(fileno(config_file), &st);
  
 -      if (chmod(lock->filename, st.st_mode & 07777) < 0) {
 +      if (chmod(lock->filename.buf, st.st_mode & 07777) < 0) {
                ret = error("chmod on %s failed: %s",
 -                              lock->filename, strerror(errno));
 +                              lock->filename.buf, strerror(errno));
                goto out;
        }
  
                                }
                                store.baselen = strlen(new_name);
                                if (!store_write_section(out_fd, new_name)) {
 -                                      ret = write_error(lock->filename);
 +                                      ret = write_error(lock->filename.buf);
                                        goto out;
                                }
                                /*
                        continue;
                length = strlen(output);
                if (write_in_full(out_fd, output, length) != length) {
 -                      ret = write_error(lock->filename);
 +                      ret = write_error(lock->filename.buf);
                        goto out;
                }
        }
diff --combined config.mak.uname
index a2f380fd8dbb852ceeb4933b1b942e0e4803f44a,0941e30a67faa3c0ab2e0ff312ca5f58c92f9dff..f3c93f27c945193754090e97c2070c9e66759f02
@@@ -89,13 -89,8 +89,13 @@@ ifeq ($(uname_S),Darwin
        NEEDS_CRYPTO_WITH_SSL = YesPlease
        NEEDS_SSL_WITH_CRYPTO = YesPlease
        NEEDS_LIBICONV = YesPlease
 +      # Note: $(uname_R) gives us the underlying Darwin version.
 +      # - MacOS 10.0.* and MacOS 10.1.0 = Darwin 1.*
 +      # - MacOS 10.x.* = Darwin (x+4).* for (1 <= x)
 +      # i.e. "begins with [15678] and a dot" means "10.4.* or older".
        ifeq ($(shell expr "$(uname_R)" : '[15678]\.'),2)
                OLD_ICONV = UnfortunatelyYes
 +              NO_APPLE_COMMON_CRYPTO = YesPlease
        endif
        ifeq ($(shell expr "$(uname_R)" : '[15]\.'),2)
                NO_STRLCPY = YesPlease
        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
@@@ -373,6 -369,7 +374,7 @@@ ifeq ($(uname_S),Windows
        EXTLIBS = user32.lib advapi32.lib shell32.lib wininet.lib ws2_32.lib invalidcontinue.obj
        PTHREAD_LIBS =
        lib =
+       BASIC_CFLAGS += -DPROTECT_NTFS_DEFAULT=1
  ifndef DEBUG
        BASIC_CFLAGS += -GL -Os -MD
        BASIC_LDFLAGS += -LTCG
@@@ -514,6 -511,7 +516,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 fsck.c
index 2fffa434a5763abb6d7895ee8c7582307766ee62,50c6507d60734a95beea11de1b7b7d5e5965e2e6..032419463126234fa855b934bae85fb27d6b26a4
--- 1/fsck.c
--- 2/fsck.c
+++ b/fsck.c
@@@ -6,7 -6,7 +6,8 @@@
  #include "commit.h"
  #include "tag.h"
  #include "fsck.h"
 +#include "refs.h"
+ #include "utf8.h"
  
  static int fsck_walk_tree(struct tree *tree, fsck_walk_func walk, void *data)
  {
@@@ -171,7 -171,9 +172,9 @@@ static int fsck_tree(struct tree *item
                has_empty_name |= !*name;
                has_dot |= !strcmp(name, ".");
                has_dotdot |= !strcmp(name, "..");
-               has_dotgit |= !strcmp(name, ".git");
+               has_dotgit |= (!strcmp(name, ".git") ||
+                              is_hfs_dotgit(name) ||
+                              is_ntfs_dotgit(name));
                has_zero_pad |= *(char *)desc.buffer == '0';
                update_tree_entry(&desc);
  
        return retval;
  }
  
 +static int require_end_of_header(const void *data, unsigned long size,
 +      struct object *obj, fsck_error error_func)
 +{
 +      const char *buffer = (const char *)data;
 +      unsigned long i;
 +
 +      for (i = 0; i < size; i++) {
 +              switch (buffer[i]) {
 +              case '\0':
 +                      return error_func(obj, FSCK_ERROR,
 +                              "unterminated header: NUL at offset %d", i);
 +              case '\n':
 +                      if (i + 1 < size && buffer[i + 1] == '\n')
 +                              return 0;
 +              }
 +      }
 +
 +      return error_func(obj, FSCK_ERROR, "unterminated header");
 +}
 +
  static int fsck_ident(const char **ident, struct object *obj, fsck_error error_func)
  {
        char *end;
  }
  
  static int fsck_commit_buffer(struct commit *commit, const char *buffer,
 -                            fsck_error error_func)
 +      unsigned long size, fsck_error error_func)
  {
        unsigned char tree_sha1[20], sha1[20];
        struct commit_graft *graft;
        unsigned parent_count, parent_line_count = 0;
        int err;
  
 +      if (require_end_of_header(buffer, size, &commit->object, error_func))
 +              return -1;
 +
        if (!skip_prefix(buffer, "tree ", &buffer))
                return error_func(&commit->object, FSCK_ERROR, "invalid format - expected 'tree' line");
        if (get_sha1_hex(buffer, tree_sha1) || buffer[40] != '\n')
        return 0;
  }
  
 -static int fsck_commit(struct commit *commit, fsck_error error_func)
 +static int fsck_commit(struct commit *commit, const char *data,
 +      unsigned long size, fsck_error error_func)
  {
 -      const char *buffer = get_commit_buffer(commit, NULL);
 -      int ret = fsck_commit_buffer(commit, buffer, error_func);
 -      unuse_commit_buffer(commit, buffer);
 +      const char *buffer = data ?  data : get_commit_buffer(commit, &size);
 +      int ret = fsck_commit_buffer(commit, buffer, size, error_func);
 +      if (!data)
 +              unuse_commit_buffer(commit, buffer);
        return ret;
  }
  
 -static int fsck_tag(struct tag *tag, fsck_error error_func)
 +static int fsck_tag_buffer(struct tag *tag, const char *data,
 +      unsigned long size, fsck_error error_func)
 +{
 +      unsigned char sha1[20];
 +      int ret = 0;
 +      const char *buffer;
 +      char *to_free = NULL, *eol;
 +      struct strbuf sb = STRBUF_INIT;
 +
 +      if (data)
 +              buffer = data;
 +      else {
 +              enum object_type type;
 +
 +              buffer = to_free =
 +                      read_sha1_file(tag->object.sha1, &type, &size);
 +              if (!buffer)
 +                      return error_func(&tag->object, FSCK_ERROR,
 +                              "cannot read tag object");
 +
 +              if (type != OBJ_TAG) {
 +                      ret = error_func(&tag->object, FSCK_ERROR,
 +                              "expected tag got %s",
 +                          typename(type));
 +                      goto done;
 +              }
 +      }
 +
 +      if (require_end_of_header(buffer, size, &tag->object, error_func))
 +              goto done;
 +
 +      if (!skip_prefix(buffer, "object ", &buffer)) {
 +              ret = error_func(&tag->object, FSCK_ERROR, "invalid format - expected 'object' line");
 +              goto done;
 +      }
 +      if (get_sha1_hex(buffer, sha1) || buffer[40] != '\n') {
 +              ret = error_func(&tag->object, FSCK_ERROR, "invalid 'object' line format - bad sha1");
 +              goto done;
 +      }
 +      buffer += 41;
 +
 +      if (!skip_prefix(buffer, "type ", &buffer)) {
 +              ret = error_func(&tag->object, FSCK_ERROR, "invalid format - expected 'type' line");
 +              goto done;
 +      }
 +      eol = strchr(buffer, '\n');
 +      if (!eol) {
 +              ret = error_func(&tag->object, FSCK_ERROR, "invalid format - unexpected end after 'type' line");
 +              goto done;
 +      }
 +      if (type_from_string_gently(buffer, eol - buffer, 1) < 0)
 +              ret = error_func(&tag->object, FSCK_ERROR, "invalid 'type' value");
 +      if (ret)
 +              goto done;
 +      buffer = eol + 1;
 +
 +      if (!skip_prefix(buffer, "tag ", &buffer)) {
 +              ret = error_func(&tag->object, FSCK_ERROR, "invalid format - expected 'tag' line");
 +              goto done;
 +      }
 +      eol = strchr(buffer, '\n');
 +      if (!eol) {
 +              ret = error_func(&tag->object, FSCK_ERROR, "invalid format - unexpected end after 'type' line");
 +              goto done;
 +      }
 +      strbuf_addf(&sb, "refs/tags/%.*s", (int)(eol - buffer), buffer);
 +      if (check_refname_format(sb.buf, 0))
 +              error_func(&tag->object, FSCK_WARN, "invalid 'tag' name: %s", buffer);
 +      buffer = eol + 1;
 +
 +      if (!skip_prefix(buffer, "tagger ", &buffer))
 +              /* early tags do not contain 'tagger' lines; warn only */
 +              error_func(&tag->object, FSCK_WARN, "invalid format - expected 'tagger' line");
 +      else
 +              ret = fsck_ident(&buffer, &tag->object, error_func);
 +
 +done:
 +      strbuf_release(&sb);
 +      free(to_free);
 +      return ret;
 +}
 +
 +static int fsck_tag(struct tag *tag, const char *data,
 +      unsigned long size, fsck_error error_func)
  {
        struct object *tagged = tag->tagged;
  
        if (!tagged)
                return error_func(&tag->object, FSCK_ERROR, "could not load tagged object");
 -      return 0;
 +
 +      return fsck_tag_buffer(tag, data, size, error_func);
  }
  
 -int fsck_object(struct object *obj, int strict, fsck_error error_func)
 +int fsck_object(struct object *obj, void *data, unsigned long size,
 +      int strict, fsck_error error_func)
  {
        if (!obj)
                return error_func(obj, FSCK_ERROR, "no valid object to fsck");
        if (obj->type == OBJ_TREE)
                return fsck_tree((struct tree *) obj, strict, error_func);
        if (obj->type == OBJ_COMMIT)
 -              return fsck_commit((struct commit *) obj, error_func);
 +              return fsck_commit((struct commit *) obj, (const char *) data,
 +                      size, error_func);
        if (obj->type == OBJ_TAG)
 -              return fsck_tag((struct tag *) obj, error_func);
 +              return fsck_tag((struct tag *) obj, (const char *) data,
 +                      size, error_func);
  
        return error_func(obj, FSCK_ERROR, "unknown type '%d' (internal fsck error)",
                          obj->type);
diff --combined path.c
index f68df0cf888124592db6c771d78d94894ffbacd9,757d0b057d3d72842e182864cc0f52b6457e4079..e6089938018b74fb8a6ea547ffc64d428a13daa0
--- 1/path.c
--- 2/path.c
+++ b/path.c
@@@ -148,12 -148,10 +148,12 @@@ void home_config_paths(char **global, c
                        *global = mkpathdup("%s/.gitconfig", home);
        }
  
 -      if (!xdg_home)
 -              *xdg = NULL;
 -      else
 -              *xdg = mkpathdup("%s/git/%s", xdg_home, file);
 +      if (xdg) {
 +              if (!xdg_home)
 +                      *xdg = NULL;
 +              else
 +                      *xdg = mkpathdup("%s/git/%s", xdg_home, file);
 +      }
  
        free(to_free);
  }
@@@ -823,3 -821,36 +823,36 @@@ int daemon_avoid_alias(const char *p
                }
        }
  }
+ 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 8f3e9eb31498d708acf63bf3fc6de3d34562d42e,f0426938565e89d870aa07b65557e7d5b890b382..9cff715d6b669ecf05f861bc553b61d20d04191d
@@@ -5,7 -5,6 +5,7 @@@
   */
  #define NO_THE_INDEX_COMPATIBILITY_MACROS
  #include "cache.h"
 +#include "lockfile.h"
  #include "cache-tree.h"
  #include "refs.h"
  #include "dir.h"
@@@ -17,6 -16,7 +17,7 @@@
  #include "varint.h"
  #include "split-index.h"
  #include "sigchain.h"
+ #include "utf8.h"
  
  static struct cache_entry *refresh_cache_entry(struct cache_entry *ce,
                                               unsigned int options);
@@@ -776,9 -776,10 +777,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 */
@@@ -802,6 -803,10 +804,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')
@@@ -1247,16 -1252,24 +1253,16 @@@ static struct cache_entry *refresh_cach
  
  #define INDEX_FORMAT_DEFAULT 3
  
 -static int index_format_config(const char *var, const char *value, void *cb)
 -{
 -      unsigned int *version = cb;
 -      if (!strcmp(var, "index.version")) {
 -              *version = git_config_int(var, value);
 -              return 0;
 -      }
 -      return 1;
 -}
 -
  static unsigned int get_index_format_default(void)
  {
        char *envversion = getenv("GIT_INDEX_VERSION");
        char *endp;
 +      int value;
        unsigned int version = INDEX_FORMAT_DEFAULT;
  
        if (!envversion) {
 -              git_config(index_format_config, &version);
 +              if (!git_config_get_int("index.version", &value))
 +                      version = value;
                if (version < INDEX_FORMAT_LB || INDEX_FORMAT_UB < version) {
                        warning(_("index.version set, but the value is invalid.\n"
                                  "Using version %i"), INDEX_FORMAT_DEFAULT);
@@@ -1368,14 -1381,6 +1374,14 @@@ static int read_index_extension(struct 
        return 0;
  }
  
 +int hold_locked_index(struct lock_file *lk, int die_on_error)
 +{
 +      return hold_lock_file_for_update(lk, get_index_file(),
 +                                       die_on_error
 +                                       ? LOCK_DIE_ON_ERROR
 +                                       : 0);
 +}
 +
  int read_index(struct index_state *istate)
  {
        return read_index_from(istate, get_index_file());
@@@ -1474,21 -1479,6 +1480,21 @@@ static struct cache_entry *create_from_
        return ce;
  }
  
 +static void check_ce_order(struct cache_entry *ce, struct cache_entry *next_ce)
 +{
 +      int name_compare = strcmp(ce->name, next_ce->name);
 +      if (0 < name_compare)
 +              die("unordered stage entries in index");
 +      if (!name_compare) {
 +              if (!ce_stage(ce))
 +                      die("multiple stage entries for merged file '%s'",
 +                              ce->name);
 +              if (ce_stage(ce) > ce_stage(next_ce))
 +                      die("unordered stage entries for '%s'",
 +                              ce->name);
 +      }
 +}
 +
  /* remember to discard_cache() before reading a different cache! */
  int do_read_index(struct index_state *istate, const char *path, int must_exist)
  {
                ce = create_from_disk(disk_ce, &consumed, previous_name);
                set_index_entry(istate, i, ce);
  
 +              if (i > 0)
 +                      check_ce_order(istate->cache[i - 1], ce);
 +
                src_offset += consumed;
        }
        strbuf_release(&previous_name_buf);
@@@ -2050,10 -2037,16 +2056,10 @@@ void set_alternate_index_output(const c
  
  static int commit_locked_index(struct lock_file *lk)
  {
 -      if (alternate_index_output) {
 -              if (lk->fd >= 0 && close_lock_file(lk))
 -                      return -1;
 -              if (rename(lk->filename, alternate_index_output))
 -                      return -1;
 -              lk->filename[0] = 0;
 -              return 0;
 -      } else {
 +      if (alternate_index_output)
 +              return commit_lock_file_to(lk, alternate_index_output);
 +      else
                return commit_lock_file(lk);
 -      }
  }
  
  static int do_write_locked_index(struct index_state *istate, struct lock_file *lock,
@@@ -2193,6 -2186,7 +2199,6 @@@ int read_index_unmerged(struct index_st
                if (add_index_entry(istate, new_ce, 0))
                        return error("%s: cannot drop to stage #0",
                                     new_ce->name);
 -              i = index_name_pos(istate, new_ce->name, len);
        }
        return unmerged;
  }
diff --combined t/t1450-fsck.sh
index 019fddd4e81ed7b2103f916a634f3e85e7291017,426f753fe3e4907a151be87be247a32bf9741905..d00b70f99deffc6a4009fcea2ef387aade68d3e6
@@@ -214,44 -214,6 +214,44 @@@ test_expect_success 'tag pointing to so
        test_must_fail git fsck --tags
  '
  
 +test_expect_success 'tag with incorrect tag name & missing tagger' '
 +      sha=$(git rev-parse HEAD) &&
 +      cat >wrong-tag <<-EOF &&
 +      object $sha
 +      type commit
 +      tag wrong name format
 +
 +      This is an invalid tag.
 +      EOF
 +
 +      tag=$(git hash-object -t tag -w --stdin <wrong-tag) &&
 +      test_when_finished "remove_object $tag" &&
 +      echo $tag >.git/refs/tags/wrong &&
 +      test_when_finished "git update-ref -d refs/tags/wrong" &&
 +      git fsck --tags 2>out &&
 +      grep "invalid .tag. name" out &&
 +      grep "expected .tagger. line" out
 +'
 +
 +test_expect_success 'tag with bad tagger' '
 +      sha=$(git rev-parse HEAD) &&
 +      cat >wrong-tag <<-EOF &&
 +      object $sha
 +      type commit
 +      tag not-quite-wrong
 +      tagger Bad Tagger Name
 +
 +      This is an invalid tag.
 +      EOF
 +
 +      tag=$(git hash-object --literally -t tag -w --stdin <wrong-tag) &&
 +      test_when_finished "remove_object $tag" &&
 +      echo $tag >.git/refs/tags/wrong &&
 +      test_when_finished "git update-ref -d refs/tags/wrong" &&
 +      test_must_fail git fsck --tags 2>out &&
 +      grep "error in tag .*: invalid author/committer" out
 +'
 +
  test_expect_success 'cleaned up' '
        git fsck >actual 2>&1 &&
        test_cmp empty actual
@@@ -309,36 -271,41 +309,41 @@@ 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
  
  # create a static test repo which is broken by omitting
  # one particular object ($1, which is looked up via rev-parse
diff --combined t/test-lib.sh
index cf19339ccebd0ee5f55822ebe11386101f358f41,a7a4639d7c61d43c2772207d118ee8897456f1ec..79e8a33d043e7208c2c1a08ab38fad207049ef4c
@@@ -169,7 -169,11 +169,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:
  #
        --root=*)
                root=$(expr "z$1" : 'z[^=]*=\(.*\)')
                shift ;;
 +      -x)
 +              trace=t
 +              verbose=t
 +              shift ;;
        *)
                echo "error: unknown test option '$1'" >&2; exit 1 ;;
        esac
@@@ -521,39 -521,10 +525,39 @@@ maybe_setup_valgrind () 
        fi
  }
  
 +# This is a separate function because some tests use
 +# "return" to end a test_expect_success block early
 +# (and we want to make sure we run any cleanup like
 +# "set +x").
 +test_eval_inner_ () {
 +      # Do not add anything extra (including LF) after '$*'
 +      eval "
 +              test \"$trace\" = t && set -x
 +              $*"
 +}
 +
  test_eval_ () {
 -      # This is a separate function because some tests use
 -      # "return" to end a test_expect_success block early.
 -      eval </dev/null >&3 2>&4 "$*"
 +      # We run this block with stderr redirected to avoid extra cruft
 +      # during a "-x" trace. Once in "set -x" mode, we cannot prevent
 +      # the shell from printing the "set +x" to turn it off (nor the saving
 +      # of $? before that). But we can make sure that the output goes to
 +      # /dev/null.
 +      #
 +      # The test itself is run with stderr put back to &4 (so either to
 +      # /dev/null, or to the original stderr if --verbose was used).
 +      {
 +              test_eval_inner_ "$@" </dev/null >&3 2>&4
 +              test_eval_ret_=$?
 +              if test "$trace" = t
 +              then
 +                      set +x
 +                      if test "$test_eval_ret_" != 0
 +                      then
 +                              say_color error >&4 "error: last command exited with \$?=$test_eval_ret_"
 +                      fi
 +              fi
 +      } 2>/dev/null
 +      return $test_eval_ret_
  }
  
  test_run_ () {
        eval_ret=$?
        teardown_malloc_check
  
 -      if test -z "$immediate" || test $eval_ret = 0 || test -n "$expecting_failure"
 +      if test -z "$immediate" || test $eval_ret = 0 ||
 +         test -n "$expecting_failure" && test "$test_cleanup" != ":"
        then
                setup_malloc_check
                test_eval_ "$test_cleanup"
@@@ -847,8 -817,7 +851,8 @@@ rm -fr "$TRASH_DIRECTORY" || 
  }
  
  HOME="$TRASH_DIRECTORY"
 -export HOME
 +GNUPGHOME="$HOME/gnupg-home-not-used"
 +export HOME GNUPGHOME
  
  if test -z "$TEST_NO_CREATE_REPO"
  then
@@@ -905,7 -874,7 +909,7 @@@ case $(uname -s) i
        # backslashes in pathspec are converted to '/'
        # exec does not inherit the PID
        test_set_prereq MINGW
 -      test_set_prereq NOT_CYGWIN
 +      test_set_prereq NATIVE_CRLF
        test_set_prereq SED_STRIPS_CR
        test_set_prereq GREP_STRIPS_CR
        GIT_TEST_CMP=mingw_test_cmp
  *CYGWIN*)
        test_set_prereq POSIXPERM
        test_set_prereq EXECKEEPSPID
 -      test_set_prereq NOT_MINGW
        test_set_prereq CYGWIN
        test_set_prereq SED_STRIPS_CR
        test_set_prereq GREP_STRIPS_CR
        test_set_prereq POSIXPERM
        test_set_prereq BSLASHPSPEC
        test_set_prereq EXECKEEPSPID
 -      test_set_prereq NOT_MINGW
 -      test_set_prereq NOT_CYGWIN
        ;;
  esac
  
diff --combined utf8.c
index 454177794929933a6ccc4d91e7d5c784f0f07bff,3b77e9765811b2dc0b7c4031b1229d432e1942d2..9a3f4ad23293bf6a3fc06a1942fd324fa1b74ded
--- 1/utf8.c
--- 2/utf8.c
+++ b/utf8.c
@@@ -239,6 -239,13 +239,6 @@@ int is_utf8(const char *text
        return 1;
  }
  
 -static void strbuf_addchars(struct strbuf *sb, int c, size_t n)
 -{
 -      strbuf_grow(sb, n);
 -      memset(sb->buf + sb->len, c, n);
 -      strbuf_setlen(sb, sb->len + n);
 -}
 -
  static void strbuf_add_indented_text(struct strbuf *buf, const char *text,
                                     int indent, int indent2)
  {
@@@ -375,9 -382,6 +375,9 @@@ void strbuf_utf8_replace(struct strbuf 
                        dst += n;
                }
  
 +              if (src >= end)
 +                      break;
 +
                old = src;
                n = utf8_width((const char**)&src, NULL);
                if (!src)       /* broken utf-8, do nothing */
@@@ -561,3 -565,67 +561,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;
+ }