Merge branch 'js/early-config'
authorJunio C Hamano <gitster@pobox.com>
Fri, 17 Mar 2017 20:50:28 +0000 (13:50 -0700)
committerJunio C Hamano <gitster@pobox.com>
Fri, 17 Mar 2017 20:50:28 +0000 (13:50 -0700)
The start-up sequence of "git" needs to figure out some configured
settings before it finds and set itself up in the location of the
repository and was quite messy due to its "chicken-and-egg" nature.
The code has been restructured.

* js/early-config:
setup.c: mention unresolved problems
t1309: document cases where we would want early config not to die()
setup_git_directory_gently_1(): avoid die()ing
t1309: test read_early_config()
read_early_config(): really discover .git/
read_early_config(): avoid .git/config hack when unneeded
setup: make read_early_config() reusable
setup: introduce the discover_git_directory() function
setup_git_directory_1(): avoid changing global state
setup: prepare setup_discovered_git_dir() for the root directory
setup_git_directory(): use is_dir_sep() helper
t7006: replace dubious test

1  2 
cache.h
config.c
setup.c
diff --combined cache.h
index 26f336b00ff1b5cd286690b797806e31b1cde27d,c12bc234e9900a36c20157fc1a13e9c2625dac56..9b2157f59112be6f9d2733d460ffe36f8937c6c7
+++ b/cache.h
@@@ -10,8 -10,8 +10,8 @@@
  #include "trace.h"
  #include "string-list.h"
  #include "pack-revindex.h"
 +#include "hash.h"
  
 -#include SHA1_HEADER
  #ifndef platform_SHA_CTX
  /*
   * platform's underlying implementation of SHA-1; could be OpenSSL,
@@@ -518,6 -518,13 +518,13 @@@ extern void set_git_work_tree(const cha
  #define ALTERNATE_DB_ENVIRONMENT "GIT_ALTERNATE_OBJECT_DIRECTORIES"
  
  extern void setup_work_tree(void);
+ /*
+  * Find GIT_DIR of the repository that contains the current working directory,
+  * without changing the working directory or other global state. The result is
+  * appended to gitdir. The return value is either NULL if no repository was
+  * found, or pointing to the path inside gitdir's buffer.
+  */
+ extern const char *discover_git_directory(struct strbuf *gitdir);
  extern const char *setup_git_directory_gently(int *);
  extern const char *setup_git_directory(void);
  extern char *prefix_path(const char *prefix, int len, const char *path);
@@@ -1045,6 -1052,9 +1052,6 @@@ static inline int is_empty_tree_oid(con
        return !hashcmp(oid->hash, EMPTY_TREE_SHA1_BIN);
  }
  
 -
 -int git_mkstemp(char *path, size_t n, const char *template);
 -
  /* set default permissions by passing mode arguments to open(2) */
  int git_mkstemps_mode(char *pattern, int suffix_len, int mode);
  int git_mkstemp_mode(char *pattern, int mode);
@@@ -1069,9 -1079,8 +1076,9 @@@ int adjust_shared_perm(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.
 + * somewhat safe against races. Return one of the scld_error values to
 + * indicate success/failure. On error, set errno to describe the
 + * problem.
   *
   * SCLD_VANISHED indicates that one of the ancestor directories of the
   * path existed at one point during the function call and then
@@@ -1095,49 -1104,6 +1102,49 @@@ enum scld_error 
  enum scld_error safe_create_leading_directories(char *path);
  enum scld_error safe_create_leading_directories_const(const char *path);
  
 +/*
 + * Callback function for raceproof_create_file(). This function is
 + * expected to do something that makes dirname(path) permanent despite
 + * the fact that other processes might be cleaning up empty
 + * directories at the same time. Usually it will create a file named
 + * path, but alternatively it could create another file in that
 + * directory, or even chdir() into that directory. The function should
 + * return 0 if the action was completed successfully. On error, it
 + * should return a nonzero result and set errno.
 + * raceproof_create_file() treats two errno values specially:
 + *
 + * - ENOENT -- dirname(path) does not exist. In this case,
 + *             raceproof_create_file() tries creating dirname(path)
 + *             (and any parent directories, if necessary) and calls
 + *             the function again.
 + *
 + * - EISDIR -- the file already exists and is a directory. In this
 + *             case, raceproof_create_file() removes the directory if
 + *             it is empty (and recursively any empty directories that
 + *             it contains) and calls the function again.
 + *
 + * Any other errno causes raceproof_create_file() to fail with the
 + * callback's return value and errno.
 + *
 + * Obviously, this function should be OK with being called again if it
 + * fails with ENOENT or EISDIR. In other scenarios it will not be
 + * called again.
 + */
 +typedef int create_file_fn(const char *path, void *cb);
 +
 +/*
 + * Create a file in dirname(path) by calling fn, creating leading
 + * directories if necessary. Retry a few times in case we are racing
 + * with another process that is trying to clean up the directory that
 + * contains path. See the documentation for create_file_fn for more
 + * details.
 + *
 + * Return the value and set the errno that resulted from the most
 + * recent call of fn. fn is always called at least once, and will be
 + * called more than once if it returns ENOENT or EISDIR.
 + */
 +int raceproof_create_file(const char *path, create_file_fn fn, void *cb);
 +
  int mkdir_in_gitdir(const char *path);
  extern char *expand_user_path(const char *path);
  const char *enter_repo(const char *path, int strict);
@@@ -1150,7 -1116,7 +1157,7 @@@ char *strbuf_realpath(struct strbuf *re
                      int die_on_error);
  const char *real_path(const char *path);
  const char *real_path_if_valid(const char *path);
 -char *real_pathdup(const char *path);
 +char *real_pathdup(const char *path, int die_on_error);
  const char *absolute_path(const char *path);
  char *absolute_pathdup(const char *path);
  const char *remove_leading_path(const char *in, const char *prefix);
@@@ -1270,9 -1236,6 +1277,9 @@@ extern int has_pack_index(const unsigne
  
  extern void assert_sha1_type(const unsigned char *sha1, enum object_type expect);
  
 +/* Helper to check and "touch" a file */
 +extern int check_and_freshen_file(const char *fn, int freshen);
 +
  extern const signed char hexval_table[256];
  static inline unsigned int hexval(unsigned char c)
  {
@@@ -1363,46 -1326,7 +1370,46 @@@ extern char *oid_to_hex_r(char *out, co
  extern char *sha1_to_hex(const unsigned char *sha1);  /* static buffer result! */
  extern char *oid_to_hex(const struct object_id *oid); /* same static buffer as sha1_to_hex */
  
 -extern int interpret_branch_name(const char *str, int len, struct strbuf *);
 +/*
 + * Parse a 40-character hexadecimal object ID starting from hex, updating the
 + * pointer specified by end when parsing stops.  The resulting object ID is
 + * stored in oid.  Returns 0 on success.  Parsing will stop on the first NUL or
 + * other invalid character.  end is only updated on success; otherwise, it is
 + * unmodified.
 + */
 +extern int parse_oid_hex(const char *hex, struct object_id *oid, const char **end);
 +
 +/*
 + * This reads short-hand syntax that not only evaluates to a commit
 + * object name, but also can act as if the end user spelled the name
 + * of the branch from the command line.
 + *
 + * - "@{-N}" finds the name of the Nth previous branch we were on, and
 + *   places the name of the branch in the given buf and returns the
 + *   number of characters parsed if successful.
 + *
 + * - "<branch>@{upstream}" finds the name of the other ref that
 + *   <branch> is configured to merge with (missing <branch> defaults
 + *   to the current branch), and places the name of the branch in the
 + *   given buf and returns the number of characters parsed if
 + *   successful.
 + *
 + * If the input is not of the accepted format, it returns a negative
 + * number to signal an error.
 + *
 + * If the input was ok but there are not N branch switches in the
 + * reflog, it returns 0.
 + *
 + * If "allowed" is non-zero, it is a treated as a bitfield of allowable
 + * expansions: local branches ("refs/heads/"), remote branches
 + * ("refs/remotes/"), or "HEAD". If no "allowed" bits are set, any expansion is
 + * allowed, even ones to refs outside of those namespaces.
 + */
 +#define INTERPRET_BRANCH_LOCAL (1<<0)
 +#define INTERPRET_BRANCH_REMOTE (1<<1)
 +#define INTERPRET_BRANCH_HEAD (1<<2)
 +extern int interpret_branch_name(const char *str, int len, struct strbuf *,
 +                               unsigned allowed);
  extern int get_oid_mb(const char *str, struct object_id *oid);
  
  extern int validate_headref(const char *ref);
@@@ -1682,12 -1606,6 +1689,12 @@@ extern void check_pack_index_ptr(const 
   * error.
   */
  extern const unsigned char *nth_packed_object_sha1(struct packed_git *, uint32_t n);
 +/*
 + * Like nth_packed_object_sha1, but write the data into the object specified by
 + * the the first argument.  Returns the first argument on success, and NULL on
 + * error.
 + */
 +extern const struct object_id *nth_packed_object_oid(struct object_id *, struct packed_git *, uint32_t n);
  
  /*
   * Return the offset of the nth object within the specified packfile.
@@@ -1729,7 -1647,7 +1736,7 @@@ extern int unpack_object_header(struct 
   * scratch buffer, but restored to its original contents before
   * the function returns.
   */
 -typedef int each_loose_object_fn(const unsigned char *sha1,
 +typedef int each_loose_object_fn(const struct object_id *oid,
                                 const char *path,
                                 void *data);
  typedef int each_loose_cruft_fn(const char *basename,
@@@ -1755,7 -1673,7 +1762,7 @@@ int for_each_loose_file_in_objdir_buf(s
   * LOCAL_ONLY flag is set).
   */
  #define FOR_EACH_OBJECT_LOCAL_ONLY 0x1
 -typedef int each_packed_object_fn(const unsigned char *sha1,
 +typedef int each_packed_object_fn(const struct object_id *oid,
                                  struct packed_git *pack,
                                  uint32_t pos,
                                  void *data);
@@@ -1842,6 -1760,7 +1849,7 @@@ extern int git_config_from_blob_sha1(co
                                     const unsigned char *sha1, void *data);
  extern void git_config_push_parameter(const char *text);
  extern int git_config_from_parameters(config_fn_t fn, void *data);
+ extern void read_early_config(config_fn_t cb, void *data);
  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,
@@@ -1908,11 -1827,8 +1916,11 @@@ extern int git_config_include(const cha
   *
   * (i.e., what gets handed to a config_fn_t). The caller provides the section;
   * we return -1 if it does not match, 0 otherwise. The subsection and key
 - * out-parameters are filled by the function (and subsection is NULL if it is
 + * out-parameters are filled by the function (and *subsection is NULL if it is
   * missing).
 + *
 + * If the subsection pointer-to-pointer passed in is NULL, returns 0 only if
 + * there is no subsection at all.
   */
  extern int parse_config_key(const char *var,
                            const char *section,
@@@ -1974,11 -1890,6 +1982,11 @@@ extern int git_config_get_bool_or_int(c
  extern int git_config_get_maybe_bool(const char *key, int *dest);
  extern int git_config_get_pathname(const char *key, const char **dest);
  extern int git_config_get_untracked_cache(void);
 +extern int git_config_get_split_index(void);
 +extern int git_config_get_max_percent_split_change(void);
 +
 +/* This dies if the configured or default date is in the future */
 +extern int git_config_get_expiry(const char *key, const char **output);
  
  /*
   * This is a hack for test programs like test-dump-untracked-cache to
diff --combined config.c
index bd286b3e3610b23e289bd1aa55e4cc2e79ec6590,a88df53fdbca9149e23993319fce6255dc3b6764..eb7e310a11e453f68c366d0212b115de43f33433
+++ b/config.c
@@@ -201,105 -201,11 +201,105 @@@ void git_config_push_parameter(const ch
        strbuf_release(&env);
  }
  
 +static inline int iskeychar(int c)
 +{
 +      return isalnum(c) || c == '-';
 +}
 +
 +/*
 + * Auxiliary function to sanity-check and split the key into the section
 + * identifier and variable name.
 + *
 + * Returns 0 on success, -1 when there is an invalid character in the key and
 + * -2 if there is no section name in the key.
 + *
 + * store_key - pointer to char* which will hold a copy of the key with
 + *             lowercase section and variable name
 + * baselen - pointer to int which will hold the length of the
 + *           section + subsection part, can be NULL
 + */
 +static int git_config_parse_key_1(const char *key, char **store_key, int *baselen_, int quiet)
 +{
 +      int i, dot, baselen;
 +      const char *last_dot = strrchr(key, '.');
 +
 +      /*
 +       * Since "key" actually contains the section name and the real
 +       * key name separated by a dot, we have to know where the dot is.
 +       */
 +
 +      if (last_dot == NULL || last_dot == key) {
 +              if (!quiet)
 +                      error("key does not contain a section: %s", key);
 +              return -CONFIG_NO_SECTION_OR_NAME;
 +      }
 +
 +      if (!last_dot[1]) {
 +              if (!quiet)
 +                      error("key does not contain variable name: %s", key);
 +              return -CONFIG_NO_SECTION_OR_NAME;
 +      }
 +
 +      baselen = last_dot - key;
 +      if (baselen_)
 +              *baselen_ = baselen;
 +
 +      /*
 +       * Validate the key and while at it, lower case it for matching.
 +       */
 +      if (store_key)
 +              *store_key = xmallocz(strlen(key));
 +
 +      dot = 0;
 +      for (i = 0; key[i]; i++) {
 +              unsigned char c = key[i];
 +              if (c == '.')
 +                      dot = 1;
 +              /* Leave the extended basename untouched.. */
 +              if (!dot || i > baselen) {
 +                      if (!iskeychar(c) ||
 +                          (i == baselen + 1 && !isalpha(c))) {
 +                              if (!quiet)
 +                                      error("invalid key: %s", key);
 +                              goto out_free_ret_1;
 +                      }
 +                      c = tolower(c);
 +              } else if (c == '\n') {
 +                      if (!quiet)
 +                              error("invalid key (newline): %s", key);
 +                      goto out_free_ret_1;
 +              }
 +              if (store_key)
 +                      (*store_key)[i] = c;
 +      }
 +
 +      return 0;
 +
 +out_free_ret_1:
 +      if (store_key) {
 +              free(*store_key);
 +              *store_key = NULL;
 +      }
 +      return -CONFIG_INVALID_KEY;
 +}
 +
 +int git_config_parse_key(const char *key, char **store_key, int *baselen)
 +{
 +      return git_config_parse_key_1(key, store_key, baselen, 0);
 +}
 +
 +int git_config_key_is_valid(const char *key)
 +{
 +      return !git_config_parse_key_1(key, NULL, NULL, 1);
 +}
 +
  int git_config_parse_parameter(const char *text,
                               config_fn_t fn, void *data)
  {
        const char *value;
 +      char *canonical_name;
        struct strbuf **pair;
 +      int ret;
  
        pair = strbuf_split_str(text, '=', 2);
        if (!pair[0])
                strbuf_list_free(pair);
                return error("bogus config parameter: %s", text);
        }
 -      strbuf_tolower(pair[0]);
 -      if (fn(pair[0]->buf, value, data) < 0) {
 -              strbuf_list_free(pair);
 -              return -1;
 +
 +      if (git_config_parse_key(pair[0]->buf, &canonical_name, NULL)) {
 +              ret = -1;
 +      } else {
 +              ret = (fn(canonical_name, value, data) < 0) ? -1 : 0;
 +              free(canonical_name);
        }
        strbuf_list_free(pair);
 -      return 0;
 +      return ret;
  }
  
  int git_config_from_parameters(config_fn_t fn, void *data)
@@@ -452,6 -356,11 +452,6 @@@ static char *parse_value(void
        }
  }
  
 -static inline int iskeychar(int c)
 -{
 -      return isalnum(c) || c == '-';
 -}
 -
  static int get_value(config_fn_t fn, void *data, struct strbuf *name)
  {
        int c;
@@@ -1503,6 -1412,31 +1503,31 @@@ static void configset_iter(struct confi
        }
  }
  
+ void read_early_config(config_fn_t cb, void *data)
+ {
+       struct strbuf buf = STRBUF_INIT;
+       git_config_with_options(cb, data, NULL, 1);
+       /*
+        * When setup_git_directory() was not yet asked to discover the
+        * GIT_DIR, we ask discover_git_directory() to figure out whether there
+        * is any repository config we should use (but unlike
+        * setup_git_directory_gently(), no global state is changed, most
+        * notably, the current working directory is still the same after the
+        * call).
+        */
+       if (!have_git_dir() && discover_git_directory(&buf)) {
+               struct git_config_source repo_config;
+               memset(&repo_config, 0, sizeof(repo_config));
+               strbuf_addstr(&buf, "/config");
+               repo_config.file = buf.buf;
+               git_config_with_options(cb, data, &repo_config, 1);
+       }
+       strbuf_release(&buf);
+ }
  static void git_config_check_init(void);
  
  void git_config(config_fn_t fn, void *data)
@@@ -1803,19 -1737,6 +1828,19 @@@ int git_config_get_pathname(const char 
        return ret;
  }
  
 +int git_config_get_expiry(const char *key, const char **output)
 +{
 +      int ret = git_config_get_string_const(key, output);
 +      if (ret)
 +              return ret;
 +      if (strcmp(*output, "now")) {
 +              unsigned long now = approxidate("now");
 +              if (approxidate(*output) >= now)
 +                      git_die_config(key, _("Invalid %s: '%s'"), key, *output);
 +      }
 +      return ret;
 +}
 +
  int git_config_get_untracked_cache(void)
  {
        int val = -1;
                if (!strcasecmp(v, "keep"))
                        return -1;
  
 -              error("unknown core.untrackedCache value '%s'; "
 -                    "using 'keep' default value", v);
 +              error(_("unknown core.untrackedCache value '%s'; "
 +                      "using 'keep' default value"), v);
                return -1;
        }
  
        return -1; /* default value */
  }
  
 +int git_config_get_split_index(void)
 +{
 +      int val;
 +
 +      if (!git_config_get_maybe_bool("core.splitindex", &val))
 +              return val;
 +
 +      return -1; /* default value */
 +}
 +
 +int git_config_get_max_percent_split_change(void)
 +{
 +      int val = -1;
 +
 +      if (!git_config_get_int("splitindex.maxpercentchange", &val)) {
 +              if (0 <= val && val <= 100)
 +                      return val;
 +
 +              return error(_("splitIndex.maxPercentChange value '%d' "
 +                             "should be between 0 and 100"), val);
 +      }
 +
 +      return -1; /* default value */
 +}
 +
  NORETURN
  void git_die_config_linenr(const char *key, const char *filename, int linenr)
  {
@@@ -2118,6 -2014,93 +2143,6 @@@ void git_config_set(const char *key, co
        git_config_set_multivar(key, value, NULL, 0);
  }
  
 -/*
 - * Auxiliary function to sanity-check and split the key into the section
 - * identifier and variable name.
 - *
 - * Returns 0 on success, -1 when there is an invalid character in the key and
 - * -2 if there is no section name in the key.
 - *
 - * store_key - pointer to char* which will hold a copy of the key with
 - *             lowercase section and variable name
 - * baselen - pointer to int which will hold the length of the
 - *           section + subsection part, can be NULL
 - */
 -static int git_config_parse_key_1(const char *key, char **store_key, int *baselen_, int quiet)
 -{
 -      int i, dot, baselen;
 -      const char *last_dot = strrchr(key, '.');
 -
 -      /*
 -       * Since "key" actually contains the section name and the real
 -       * key name separated by a dot, we have to know where the dot is.
 -       */
 -
 -      if (last_dot == NULL || last_dot == key) {
 -              if (!quiet)
 -                      error("key does not contain a section: %s", key);
 -              return -CONFIG_NO_SECTION_OR_NAME;
 -      }
 -
 -      if (!last_dot[1]) {
 -              if (!quiet)
 -                      error("key does not contain variable name: %s", key);
 -              return -CONFIG_NO_SECTION_OR_NAME;
 -      }
 -
 -      baselen = last_dot - key;
 -      if (baselen_)
 -              *baselen_ = baselen;
 -
 -      /*
 -       * Validate the key and while at it, lower case it for matching.
 -       */
 -      if (store_key)
 -              *store_key = xmallocz(strlen(key));
 -
 -      dot = 0;
 -      for (i = 0; key[i]; i++) {
 -              unsigned char c = key[i];
 -              if (c == '.')
 -                      dot = 1;
 -              /* Leave the extended basename untouched.. */
 -              if (!dot || i > baselen) {
 -                      if (!iskeychar(c) ||
 -                          (i == baselen + 1 && !isalpha(c))) {
 -                              if (!quiet)
 -                                      error("invalid key: %s", key);
 -                              goto out_free_ret_1;
 -                      }
 -                      c = tolower(c);
 -              } else if (c == '\n') {
 -                      if (!quiet)
 -                              error("invalid key (newline): %s", key);
 -                      goto out_free_ret_1;
 -              }
 -              if (store_key)
 -                      (*store_key)[i] = c;
 -      }
 -
 -      return 0;
 -
 -out_free_ret_1:
 -      if (store_key) {
 -              free(*store_key);
 -              *store_key = NULL;
 -      }
 -      return -CONFIG_INVALID_KEY;
 -}
 -
 -int git_config_parse_key(const char *key, char **store_key, int *baselen)
 -{
 -      return git_config_parse_key_1(key, store_key, baselen, 0);
 -}
 -
 -int git_config_key_is_valid(const char *key)
 -{
 -      return !git_config_parse_key_1(key, NULL, NULL, 1);
 -}
 -
  /*
   * If value==NULL, unset in (remove from) config,
   * if value_regex!=NULL, disregard key/value pairs where value does not match.
@@@ -2578,10 -2561,11 +2603,10 @@@ int parse_config_key(const char *var
                     const char **subsection, int *subsection_len,
                     const char **key)
  {
 -      int section_len = strlen(section);
        const char *dot;
  
        /* Does it start with "section." ? */
 -      if (!starts_with(var, section) || var[section_len] != '.')
 +      if (!skip_prefix(var, section, &var) || *var != '.')
                return -1;
  
        /*
        *key = dot + 1;
  
        /* Did we have a subsection at all? */
 -      if (dot == var + section_len) {
 -              *subsection = NULL;
 -              *subsection_len = 0;
 +      if (dot == var) {
 +              if (subsection) {
 +                      *subsection = NULL;
 +                      *subsection_len = 0;
 +              }
        }
        else {
 -              *subsection = var + section_len + 1;
 +              if (!subsection)
 +                      return -1;
 +              *subsection = var + 1;
                *subsection_len = dot - *subsection;
        }
  
diff --combined setup.c
index 8f64fbdfb28fc2e487cfdba76561e5b3a25766f0,98b8dee8b822f200c283085c36ea36b4ad54e354..64f922a9378cc48c72b9173e7c45296bdb885678
+++ b/setup.c
@@@ -254,7 -254,7 +254,7 @@@ int get_common_dir_noenv(struct strbuf 
                if (!is_absolute_path(data.buf))
                        strbuf_addf(&path, "%s/", gitdir);
                strbuf_addbuf(&path, &data);
 -              strbuf_addstr(sb, real_path(path.buf));
 +              strbuf_add_real_path(sb, path.buf);
                ret = 1;
        } else {
                strbuf_addstr(sb, gitdir);
@@@ -531,6 -531,7 +531,7 @@@ const char *read_gitfile_gently(const c
        ssize_t len;
  
        if (stat(path, &st)) {
+               /* NEEDSWORK: discern between ENOENT vs other errors */
                error_code = READ_GITFILE_ERR_STAT_FAILED;
                goto cleanup_return;
        }
@@@ -698,7 -699,7 +699,7 @@@ static const char *setup_discovered_git
        /* --work-tree is set without --git-dir; use discovered one */
        if (getenv(GIT_WORK_TREE_ENVIRONMENT) || git_work_tree_cfg) {
                if (offset != cwd->len && !is_absolute_path(gitdir))
 -                      gitdir = real_pathdup(gitdir);
 +                      gitdir = real_pathdup(gitdir, 1);
                if (chdir(cwd->buf))
                        die_errno("Could not come back to cwd");
                return setup_explicit_git_dir(gitdir, cwd, nongit_ok);
        if (offset == cwd->len)
                return NULL;
  
-       /* Make "offset" point to past the '/', and add a '/' at the end */
-       offset++;
+       /* Make "offset" point past the '/' (already the case for root dirs) */
+       if (offset != offset_1st_component(cwd->buf))
+               offset++;
+       /* Add a '/' at the end */
        strbuf_addch(cwd, '/');
        return cwd->buf + offset;
  }
@@@ -806,7 -809,7 +809,7 @@@ static int canonicalize_ceiling_entry(s
                /* Keep entry but do not canonicalize it */
                return 1;
        } else {
 -              char *real_path = real_pathdup(ceil);
 +              char *real_path = real_pathdup(ceil, 0);
                if (!real_path) {
                        return 0;
                }
        }
  }
  
+ enum discovery_result {
+       GIT_DIR_NONE = 0,
+       GIT_DIR_EXPLICIT,
+       GIT_DIR_DISCOVERED,
+       GIT_DIR_BARE,
+       /* these are errors */
+       GIT_DIR_HIT_CEILING = -1,
+       GIT_DIR_HIT_MOUNT_POINT = -2,
+       GIT_DIR_INVALID_GITFILE = -3
+ };
  /*
   * We cannot decide in this function whether we are in the work tree or
   * not, since the config can only be read _after_ this function was called.
+  *
+  * Also, we avoid changing any global state (such as the current working
+  * directory) to allow early callers.
+  *
+  * The directory where the search should start needs to be passed in via the
+  * `dir` parameter; upon return, the `dir` buffer will contain the path of
+  * the directory where the search ended, and `gitdir` will contain the path of
+  * the discovered .git/ directory, if any. If `gitdir` is not absolute, it
+  * is relative to `dir` (i.e. *not* necessarily the cwd).
   */
- static const char *setup_git_directory_gently_1(int *nongit_ok)
+ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,
+                                                         struct strbuf *gitdir,
+                                                         int die_on_error)
  {
        const char *env_ceiling_dirs = getenv(CEILING_DIRECTORIES_ENVIRONMENT);
        struct string_list ceiling_dirs = STRING_LIST_INIT_DUP;
-       static struct strbuf cwd = STRBUF_INIT;
-       const char *gitdirenv, *ret;
-       char *gitfile;
-       int offset, offset_parent, ceil_offset = -1;
+       const char *gitdirenv;
+       int ceil_offset = -1, min_offset = has_dos_drive_prefix(dir->buf) ? 3 : 1;
        dev_t current_device = 0;
        int one_filesystem = 1;
  
-       /*
-        * We may have read an incomplete configuration before
-        * setting-up the git directory. If so, clear the cache so
-        * that the next queries to the configuration reload complete
-        * configuration (including the per-repo config file that we
-        * ignored previously).
-        */
-       git_config_clear();
-       /*
-        * Let's assume that we are in a git repository.
-        * If it turns out later that we are somewhere else, the value will be
-        * updated accordingly.
-        */
-       if (nongit_ok)
-               *nongit_ok = 0;
-       if (strbuf_getcwd(&cwd))
-               die_errno(_("Unable to read current working directory"));
-       offset = cwd.len;
        /*
         * If GIT_DIR is set explicitly, we're not going
         * to do any discovery, but we still do repository
         * validation.
         */
        gitdirenv = getenv(GIT_DIR_ENVIRONMENT);
-       if (gitdirenv)
-               return setup_explicit_git_dir(gitdirenv, &cwd, nongit_ok);
+       if (gitdirenv) {
+               strbuf_addstr(gitdir, gitdirenv);
+               return GIT_DIR_EXPLICIT;
+       }
  
        if (env_ceiling_dirs) {
                int empty_entry_found = 0;
                string_list_split(&ceiling_dirs, env_ceiling_dirs, PATH_SEP, -1);
                filter_string_list(&ceiling_dirs, 0,
                                   canonicalize_ceiling_entry, &empty_entry_found);
-               ceil_offset = longest_ancestor_length(cwd.buf, &ceiling_dirs);
+               ceil_offset = longest_ancestor_length(dir->buf, &ceiling_dirs);
                string_list_clear(&ceiling_dirs, 0);
        }
  
-       if (ceil_offset < 0 && has_dos_drive_prefix(cwd.buf))
-               ceil_offset = 1;
+       if (ceil_offset < 0)
+               ceil_offset = min_offset - 2;
  
        /*
-        * Test in the following order (relative to the cwd):
+        * Test in the following order (relative to the dir):
         * - .git (file containing "gitdir: <path>")
         * - .git/
         * - ./ (bare)
         */
        one_filesystem = !git_env_bool("GIT_DISCOVERY_ACROSS_FILESYSTEM", 0);
        if (one_filesystem)
-               current_device = get_device_or_die(".", NULL, 0);
+               current_device = get_device_or_die(dir->buf, NULL, 0);
        for (;;) {
-               gitfile = (char*)read_gitfile(DEFAULT_GIT_DIR_ENVIRONMENT);
-               if (gitfile)
-                       gitdirenv = gitfile = xstrdup(gitfile);
-               else {
-                       if (is_git_directory(DEFAULT_GIT_DIR_ENVIRONMENT))
-                               gitdirenv = DEFAULT_GIT_DIR_ENVIRONMENT;
+               int offset = dir->len, error_code = 0;
+               if (offset > min_offset)
+                       strbuf_addch(dir, '/');
+               strbuf_addstr(dir, DEFAULT_GIT_DIR_ENVIRONMENT);
+               gitdirenv = read_gitfile_gently(dir->buf, die_on_error ?
+                                               NULL : &error_code);
+               if (!gitdirenv) {
+                       if (die_on_error ||
+                           error_code == READ_GITFILE_ERR_NOT_A_FILE) {
+                               /* NEEDSWORK: fail if .git is not file nor dir */
+                               if (is_git_directory(dir->buf))
+                                       gitdirenv = DEFAULT_GIT_DIR_ENVIRONMENT;
+                       } else if (error_code != READ_GITFILE_ERR_STAT_FAILED)
+                               return GIT_DIR_INVALID_GITFILE;
                }
+               strbuf_setlen(dir, offset);
                if (gitdirenv) {
-                       ret = setup_discovered_git_dir(gitdirenv,
-                                                      &cwd, offset,
-                                                      nongit_ok);
-                       free(gitfile);
-                       return ret;
+                       strbuf_addstr(gitdir, gitdirenv);
+                       return GIT_DIR_DISCOVERED;
                }
-               free(gitfile);
  
-               if (is_git_directory("."))
-                       return setup_bare_git_dir(&cwd, offset, nongit_ok);
-               offset_parent = offset;
-               while (--offset_parent > ceil_offset && cwd.buf[offset_parent] != '/');
-               if (offset_parent <= ceil_offset)
-                       return setup_nongit(cwd.buf, nongit_ok);
-               if (one_filesystem) {
-                       dev_t parent_device = get_device_or_die("..", cwd.buf,
-                                                               offset);
-                       if (parent_device != current_device) {
-                               if (nongit_ok) {
-                                       if (chdir(cwd.buf))
-                                               die_errno(_("Cannot come back to cwd"));
-                                       *nongit_ok = 1;
-                                       return NULL;
-                               }
-                               strbuf_setlen(&cwd, offset);
-                               die(_("Not a git repository (or any parent up to mount point %s)\n"
-                               "Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set)."),
-                                   cwd.buf);
-                       }
-               }
-               if (chdir("..")) {
-                       strbuf_setlen(&cwd, offset);
-                       die_errno(_("Cannot change to '%s/..'"), cwd.buf);
+               if (is_git_directory(dir->buf)) {
+                       strbuf_addstr(gitdir, ".");
+                       return GIT_DIR_BARE;
                }
-               offset = offset_parent;
+               if (offset <= min_offset)
+                       return GIT_DIR_HIT_CEILING;
+               while (--offset > ceil_offset && !is_dir_sep(dir->buf[offset]))
+                       ; /* continue */
+               if (offset <= ceil_offset)
+                       return GIT_DIR_HIT_CEILING;
+               strbuf_setlen(dir, offset > min_offset ?  offset : min_offset);
+               if (one_filesystem &&
+                   current_device != get_device_or_die(dir->buf, NULL, offset))
+                       return GIT_DIR_HIT_MOUNT_POINT;
        }
  }
  
+ const char *discover_git_directory(struct strbuf *gitdir)
+ {
+       struct strbuf dir = STRBUF_INIT, err = STRBUF_INIT;
+       size_t gitdir_offset = gitdir->len, cwd_len;
+       struct repository_format candidate;
+       if (strbuf_getcwd(&dir))
+               return NULL;
+       cwd_len = dir.len;
+       if (setup_git_directory_gently_1(&dir, gitdir, 0) <= 0) {
+               strbuf_release(&dir);
+               return NULL;
+       }
+       /*
+        * The returned gitdir is relative to dir, and if dir does not reflect
+        * the current working directory, we simply make the gitdir absolute.
+        */
+       if (dir.len < cwd_len && !is_absolute_path(gitdir->buf + gitdir_offset)) {
+               /* Avoid a trailing "/." */
+               if (!strcmp(".", gitdir->buf + gitdir_offset))
+                       strbuf_setlen(gitdir, gitdir_offset);
+               else
+                       strbuf_addch(&dir, '/');
+               strbuf_insert(gitdir, gitdir_offset, dir.buf, dir.len);
+       }
+       strbuf_reset(&dir);
+       strbuf_addf(&dir, "%s/config", gitdir->buf + gitdir_offset);
+       read_repository_format(&candidate, dir.buf);
+       strbuf_release(&dir);
+       if (verify_repository_format(&candidate, &err) < 0) {
+               warning("ignoring git dir '%s': %s",
+                       gitdir->buf + gitdir_offset, err.buf);
+               strbuf_release(&err);
+               return NULL;
+       }
+       return gitdir->buf + gitdir_offset;
+ }
  const char *setup_git_directory_gently(int *nongit_ok)
  {
+       static struct strbuf cwd = STRBUF_INIT;
+       struct strbuf dir = STRBUF_INIT, gitdir = STRBUF_INIT;
        const char *prefix;
  
-       prefix = setup_git_directory_gently_1(nongit_ok);
+       /*
+        * We may have read an incomplete configuration before
+        * setting-up the git directory. If so, clear the cache so
+        * that the next queries to the configuration reload complete
+        * configuration (including the per-repo config file that we
+        * ignored previously).
+        */
+       git_config_clear();
+       /*
+        * Let's assume that we are in a git repository.
+        * If it turns out later that we are somewhere else, the value will be
+        * updated accordingly.
+        */
+       if (nongit_ok)
+               *nongit_ok = 0;
+       if (strbuf_getcwd(&cwd))
+               die_errno(_("Unable to read current working directory"));
+       strbuf_addbuf(&dir, &cwd);
+       switch (setup_git_directory_gently_1(&dir, &gitdir, 1)) {
+       case GIT_DIR_NONE:
+               prefix = NULL;
+               break;
+       case GIT_DIR_EXPLICIT:
+               prefix = setup_explicit_git_dir(gitdir.buf, &cwd, nongit_ok);
+               break;
+       case GIT_DIR_DISCOVERED:
+               if (dir.len < cwd.len && chdir(dir.buf))
+                       die(_("Cannot change to '%s'"), dir.buf);
+               prefix = setup_discovered_git_dir(gitdir.buf, &cwd, dir.len,
+                                                 nongit_ok);
+               break;
+       case GIT_DIR_BARE:
+               if (dir.len < cwd.len && chdir(dir.buf))
+                       die(_("Cannot change to '%s'"), dir.buf);
+               prefix = setup_bare_git_dir(&cwd, dir.len, nongit_ok);
+               break;
+       case GIT_DIR_HIT_CEILING:
+               prefix = setup_nongit(cwd.buf, nongit_ok);
+               break;
+       case GIT_DIR_HIT_MOUNT_POINT:
+               if (nongit_ok) {
+                       *nongit_ok = 1;
+                       strbuf_release(&cwd);
+                       strbuf_release(&dir);
+                       return NULL;
+               }
+               die(_("Not a git repository (or any parent up to mount point %s)\n"
+                     "Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set)."),
+                   dir.buf);
+       default:
+               die("BUG: unhandled setup_git_directory_1() result");
+       }
        if (prefix)
                setenv(GIT_PREFIX_ENVIRONMENT, prefix, 1);
        else
        startup_info->have_repository = !nongit_ok || !*nongit_ok;
        startup_info->prefix = prefix;
  
+       strbuf_release(&dir);
+       strbuf_release(&gitdir);
        return prefix;
  }