For historical reasons, Git searches for the .git/ directory (or the
.git file) by changing the working directory successively to the parent
directory of the current directory, until either anything was found or
until a ceiling or a mount point is hit.

Further global state may be changed in case a .git/ directory was found.

We do have a use case, though, where we would like to find the .git/
directory without having any global state touched, though: when we read
the early config e.g. for the pager or for alias expansion.

Let's just move all of code that changes any global state out of the
function `setup_git_directory_gently_1()` into
`setup_git_directory_gently()`.

In subsequent patches, we will use the _1() function in a new
`discover_git_directory()` function that we will then use for the early
config code.

Note: the new loop is a *little* tricky, as we have to handle the root
directory specially: we cannot simply strip away the last component
including the slash, as the root directory only has that slash. To remedy
that, we introduce the `min_offset` variable that holds the minimal length
of an absolute path, and using that to special-case the root directory,
including an early exit before trying to find the parent of the root
directory.

Signed-off-by: Johannes Schindelin <johannes.schinde...@gmx.de>
---
 setup.c | 187 ++++++++++++++++++++++++++++++++++++++--------------------------
 1 file changed, 112 insertions(+), 75 deletions(-)

diff --git a/setup.c b/setup.c
index 20a1f0f870e..d7af343d14e 100644
--- a/setup.c
+++ b/setup.c
@@ -818,50 +818,49 @@ static int canonicalize_ceiling_entry(struct 
string_list_item *item,
        }
 }
 
+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
+};
+
 /*
  * 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. This path may be relative against
+ * `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)
 {
        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;
@@ -869,15 +868,15 @@ static const char *setup_git_directory_gently_1(int 
*nongit_ok)
                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)
@@ -889,63 +888,101 @@ static const char *setup_git_directory_gently_1(int 
*nongit_ok)
         */
        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;
+
+               if (offset > min_offset)
+                       strbuf_addch(dir, '/');
+               strbuf_addstr(dir, DEFAULT_GIT_DIR_ENVIRONMENT);
+               gitdirenv = read_gitfile(dir->buf);
+               if (!gitdirenv && is_git_directory(dir->buf))
+                       gitdirenv = DEFAULT_GIT_DIR_ENVIRONMENT;
+               strbuf_setlen(dir, offset);
+               if (gitdirenv) {
+                       strbuf_addstr(gitdir, gitdirenv);
+                       return GIT_DIR_DISCOVERED;
                }
 
-               if (gitdirenv) {
-                       ret = setup_discovered_git_dir(gitdirenv,
-                                                      &cwd, offset,
-                                                      nongit_ok);
-                       free(gitfile);
-                       return ret;
+               if (is_git_directory(dir->buf)) {
+                       strbuf_addstr(gitdir, ".");
+                       return GIT_DIR_BARE;
                }
-               free(gitfile);
 
-               if (is_git_directory("."))
-                       return setup_bare_git_dir(&cwd, offset, nongit_ok);
+               if (offset <= min_offset)
+                       return GIT_DIR_HIT_CEILING;
 
-               offset_parent = offset;
-               while (--offset_parent > ceil_offset &&
-                      !is_dir_sep(cwd.buf[offset_parent]))
+               while (--offset > ceil_offset && !is_dir_sep(dir->buf[offset]))
                        ; /* continue */
-               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);
-               }
-               offset = offset_parent;
+               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 *setup_git_directory_gently(int *nongit_ok)
 {
+       struct strbuf cwd = STRBUF_INIT, 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)) {
+       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;
+                       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
-- 
2.12.0.windows.1.7.g94dafc3b124


Reply via email to