Subject: [PATCH v3] Add support for GIT_CEILING_DIRS

From: David Reiss <[email-masked]>
Date: Thu, 15 May 2008 11:49:44 -0700

Make git recognize a new environment variable that prevents it from
chdir'ing up into specified directories when looking for a GIT_DIR.
Useful for avoiding slow network directories.

For example, I use git in an environment where homedirs are automounted
and "ls /home/nonexistent" takes about 9 seconds.  Setting
GIT_CEILING_DIRS="/home" allows "git help -a" (for bash completion) and
"git symbolic-ref" (for my shell prompt) to run in a reasonable time.

This also moves the chdir call to after computing the new cwd.
This should be a no-op because the cwd is not read in the interim
and any nonlocal exits either chdir to an absolute path or die.

Signed-off-by: David Reiss <[email-masked]>
---
 Documentation/git.txt   |    8 +++
 cache.h                 |    1 +
 setup.c                 |  127 ++++++++++++++++++++++++++++++++++----
 t/t1504-ceiling-dirs.sh |  156 +++++++++++++++++++++++++++++++++++++++++++++++
 t/test-lib.sh           |    1 +
 5 files changed, 281 insertions(+), 12 deletions(-)
 create mode 100755 t/t1504-ceiling-dirs.sh

diff --git a/Documentation/git.txt b/Documentation/git.txt
index 6f445b1..8aea331 100644
--- a/Documentation/git.txt
+++ b/Documentation/git.txt
@@ -415,6 +415,14 @@ git so take care if using Cogito etc.
 	This can also be controlled by the '--work-tree' command line
 	option and the core.worktree configuration variable.
 
+'GIT_CEILING_DIRS'::
+	This should be a colon-separated list of absolute paths.
+	If set, it is a list of directories that git should not chdir
+	up into while looking for a repository directory.
+	It will not exclude the current working directory or
+	a GIT_DIR set on the command line or in the environment.
+	(Useful for excluding slow-loading network directories.)
+
 git Commits
 ~~~~~~~~~~~
 'GIT_AUTHOR_NAME'::
diff --git a/cache.h b/cache.h
index 9cee9a5..8300acc 100644
--- a/cache.h
+++ b/cache.h
@@ -300,6 +300,7 @@ static inline enum object_type object_type(unsigned int mode)
 #define CONFIG_ENVIRONMENT "GIT_CONFIG"
 #define CONFIG_LOCAL_ENVIRONMENT "GIT_CONFIG_LOCAL"
 #define EXEC_PATH_ENVIRONMENT "GIT_EXEC_PATH"
+#define CEILING_DIRS_ENVIRONMENT "GIT_CEILING_DIRS"
 #define GITATTRIBUTES_FILE ".gitattributes"
 #define INFOATTRIBUTES_FILE "info/attributes"
 #define ATTRIBUTE_MACRO_PREFIX "[attr]"
diff --git a/setup.c b/setup.c
index b8fd476..fdcfae1 100644
--- a/setup.c
+++ b/setup.c
@@ -353,16 +353,118 @@ const char *read_gitfile_gently(const char *path)
 }
 
 /*
+ * path = Canonical absolute path
+ * prefix_list = Colon-separated list of canonical absolute paths
+ *
+ * Determines, for each path in parent_list, whether the "prefix" really
+ * is an ancestor directory of path.  Returns the length of the longest
+ * ancestor directory, excluding any trailing slashes, or -1 if no prefix
+ * is an ancestry.  (Note that this means 0 is returned if prefix_list
+ * contains "/".)  "/foo" is not considered an ancestor of "/foobar".
+ * Directories are not considered to be their own ancestors.  Paths must
+ * be in a canonical form: empty components, or "." or ".." components
+ * are not allowed.  prefix_list may be null, which is like "".
+ */
+static int longest_ancestor_length(const char *path, const char *prefix_list)
+{
+	const char *ceil, *colon;
+	int max_len = -1;
+
+	if (prefix_list == NULL)
+		return -1;
+	/* "/" is a tricky edge case.  It should always return -1, though. */
+	if (!strcmp(path, "/"))
+		return -1;
+
+	ceil = prefix_list;
+	for (;;) {
+		int len;
+
+		/* Add strchrnul to compat? */
+		colon = strchr(ceil, ':');
+		if (colon)
+			len = colon - ceil;
+		else
+			len = strlen(ceil);
+
+		/* "" would otherwise be treated like "/". */
+		if (len) {
+			/* Trim trailing slashes. */
+			while (len && ceil[len-1] == '/')
+				len--;
+
+			if (!strncmp(path, ceil, len) &&
+					path[len] == '/' &&
+					len > max_len) {
+				max_len = len;
+			}
+		}
+
+		if (!colon)
+			break;
+		ceil = colon + 1;
+	}
+
+	return max_len;
+}
+
+#if 0
+static void test_longest_ancestor_length()
+{
+	assert(longest_ancestor_length("/", NULL           ) == -1);
+	assert(longest_ancestor_length("/", ""             ) == -1);
+	assert(longest_ancestor_length("/", "/"            ) == -1);
+
+	assert(longest_ancestor_length("/foo", NULL           ) == -1);
+	assert(longest_ancestor_length("/foo", ""             ) == -1);
+	assert(longest_ancestor_length("/foo", ":"            ) == -1);
+	assert(longest_ancestor_length("/foo", "/"            ) ==  0);
+	assert(longest_ancestor_length("/foo", "/fo"          ) == -1);
+	assert(longest_ancestor_length("/foo", "/foo"         ) == -1);
+	assert(longest_ancestor_length("/foo", "/foo/"        ) == -1);
+	assert(longest_ancestor_length("/foo", "/bar"         ) == -1);
+	assert(longest_ancestor_length("/foo", "/bar/"        ) == -1);
+	assert(longest_ancestor_length("/foo", "/foo/bar"     ) == -1);
+	assert(longest_ancestor_length("/foo", "/foo:/bar/"   ) == -1);
+	assert(longest_ancestor_length("/foo", "/foo/:/bar/"  ) == -1);
+	assert(longest_ancestor_length("/foo", "/foo::/bar/"  ) == -1);
+	assert(longest_ancestor_length("/foo", "/:/foo:/bar/" ) ==  0);
+	assert(longest_ancestor_length("/foo", "/foo:/:/bar/" ) ==  0);
+	assert(longest_ancestor_length("/foo", "/:/bar/:/foo" ) ==  0);
+
+	assert(longest_ancestor_length("/foo/bar", NULL           ) == -1);
+	assert(longest_ancestor_length("/foo/bar", ""             ) == -1);
+	assert(longest_ancestor_length("/foo/bar", "/"            ) ==  0);
+	assert(longest_ancestor_length("/foo/bar", "/fo"          ) == -1);
+	assert(longest_ancestor_length("/foo/bar", "/foo"         ) ==  4);
+	assert(longest_ancestor_length("/foo/bar", "/foo/"        ) ==  4);
+	assert(longest_ancestor_length("/foo/bar", "/foo/ba"      ) == -1);
+	assert(longest_ancestor_length("/foo/bar", "/:/fo"        ) ==  0);
+	assert(longest_ancestor_length("/foo/bar", "/foo:/foo/ba" ) ==  4);
+	assert(longest_ancestor_length("/foo/bar", "/bar"         ) == -1);
+	assert(longest_ancestor_length("/foo/bar", "/bar/"        ) == -1);
+	assert(longest_ancestor_length("/foo/bar", "/fo:"         ) == -1);
+	assert(longest_ancestor_length("/foo/bar", ":/fo"         ) == -1);
+	assert(longest_ancestor_length("/foo/bar", "/foo:/bar/"   ) ==  4);
+	assert(longest_ancestor_length("/foo/bar", "/:/foo:/bar/" ) ==  4);
+	assert(longest_ancestor_length("/foo/bar", "/foo:/:/bar/" ) ==  4);
+	assert(longest_ancestor_length("/foo/bar", "/:/bar/:/fo"  ) ==  0);
+	assert(longest_ancestor_length("/foo/bar", "/:/bar/"      ) ==  0);
+}
+#endif
+
+/*
  * 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.
  */
 const char *setup_git_directory_gently(int *nongit_ok)
 {
 	const char *work_tree_env = getenv(GIT_WORK_TREE_ENVIRONMENT);
+	const char *env_ceiling_dirs = getenv(CEILING_DIRS_ENVIRONMENT);
 	static char cwd[PATH_MAX+1];
 	const char *gitdirenv;
 	const char *gitfile_dir;
-	int len, offset;
+	int len, offset, ceil_offset;
 
 	/*
 	 * Let's assume that we are in a git repository.
@@ -414,6 +516,8 @@ const char *setup_git_directory_gently(int *nongit_ok)
 	if (!getcwd(cwd, sizeof(cwd)-1))
 		die("Unable to read current working directory");
 
+	ceil_offset = longest_ancestor_length(cwd, env_ceiling_dirs);
+
 	/*
 	 * Test in the following order (relative to the cwd):
 	 * - .git (file containing "gitdir: <path>")
@@ -443,18 +547,17 @@ const char *setup_git_directory_gently(int *nongit_ok)
 			check_repository_format_gently(nongit_ok);
 			return NULL;
 		}
-		chdir("..");
-		do {
-			if (!offset) {
-				if (nongit_ok) {
-					if (chdir(cwd))
-						die("Cannot come back to cwd");
-					*nongit_ok = 1;
-					return NULL;
-				}
-				die("Not a git repository");
+		while (--offset > ceil_offset && cwd[offset] != '/') /* EMPTY */;
+		if (offset <= ceil_offset) {
+			if (nongit_ok) {
+				if (chdir(cwd))
+					die("Cannot come back to cwd");
+				*nongit_ok = 1;
+				return NULL;
 			}
-		} while (cwd[--offset] != '/');
+			die("Not a git repository");
+		}
+		chdir("..");
 	}
 
 	inside_git_dir = 0;
diff --git a/t/t1504-ceiling-dirs.sh b/t/t1504-ceiling-dirs.sh
new file mode 100755
index 0000000..091baad
--- /dev/null
+++ b/t/t1504-ceiling-dirs.sh
@@ -0,0 +1,156 @@
+#!/bin/sh
+
+test_description='test GIT_CEILING_DIRS'
+. ./test-lib.sh
+
+test_prefix() {
+	test_expect_success "$1" \
+	"test '$2' = \"\$(git rev-parse --show-prefix)\""
+}
+
+test_fail() {
+	test_expect_code 128 "$1: prefix" \
+	"git rev-parse --show-prefix"
+}
+
+TRASH_ROOT="$(pwd)"
+ROOT_PARENT=$(dirname "$TRASH_ROOT")
+
+
+unset GIT_CEILING_DIRS
+test_prefix no_ceil ""
+
+export GIT_CEILING_DIRS=""
+test_prefix ceil_empty ""
+
+export GIT_CEILING_DIRS="$ROOT_PARENT"
+test_prefix ceil_at_parent ""
+
+export GIT_CEILING_DIRS="$ROOT_PARENT/"
+test_prefix ceil_at_parent_slash ""
+
+export GIT_CEILING_DIRS="$TRASH_ROOT"
+test_prefix ceil_at_trash ""
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/"
+test_prefix ceil_at_trash_slash ""
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/sub"
+test_prefix ceil_at_sub ""
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/sub/"
+test_prefix ceil_at_sub_slash ""
+
+
+mkdir -p sub/dir || exit 1
+cd sub/dir || exit 1
+
+unset GIT_CEILING_DIRS
+test_prefix subdir_no_ceil "sub/dir/"
+
+export GIT_CEILING_DIRS=""
+test_prefix subdir_ceil_empty "sub/dir/"
+
+export GIT_CEILING_DIRS="$TRASH_ROOT"
+test_fail subdir_ceil_at_trash
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/"
+test_fail subdir_ceil_at_trash_slash
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/sub"
+test_fail subdir_ceil_at_sub
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/sub/"
+test_fail subdir_ceil_at_sub_slash
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/sub/dir"
+test_prefix subdir_ceil_at_subdir "sub/dir/"
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/sub/dir/"
+test_prefix subdir_ceil_at_subdir_slash "sub/dir/"
+
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/su"
+test_prefix subdir_ceil_at_su "sub/dir/"
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/su/"
+test_prefix subdir_ceil_at_su_slash "sub/dir/"
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/sub/di"
+test_prefix subdir_ceil_at_sub_di "sub/dir/"
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/sub/di"
+test_prefix subdir_ceil_at_sub_di_slash "sub/dir/"
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/subdi"
+test_prefix subdir_ceil_at_subdi "sub/dir/"
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/subdi"
+test_prefix subdir_ceil_at_subdi_slash "sub/dir/"
+
+
+export GIT_CEILING_DIRS="foo:$TRASH_ROOT/sub"
+test_fail second_of_two
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/sub:bar"
+test_fail first_of_two
+
+export GIT_CEILING_DIRS="foo:$TRASH_ROOT/sub:bar"
+test_fail second_of_three
+
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/sub"
+export GIT_DIR=../../.git
+test_prefix git_dir_specified ""
+unset GIT_DIR
+
+
+cd ../.. || exit 1
+mkdir -p s/d || exit 1
+cd s/d || exit 1
+
+unset GIT_CEILING_DIRS
+test_prefix sd_no_ceil "s/d/"
+
+export GIT_CEILING_DIRS=""
+test_prefix sd_ceil_empty "s/d/"
+
+export GIT_CEILING_DIRS="$TRASH_ROOT"
+test_fail sd_ceil_at_trash
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/"
+test_fail sd_ceil_at_trash_slash
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/s"
+test_fail sd_ceil_at_s
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/s/"
+test_fail sd_ceil_at_s_slash
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/s/d"
+test_prefix sd_ceil_at_sd "s/d/"
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/s/d/"
+test_prefix sd_ceil_at_sd_slash "s/d/"
+
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/su"
+test_prefix sd_ceil_at_su "s/d/"
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/su/"
+test_prefix sd_ceil_at_su_slash "s/d/"
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/s/di"
+test_prefix sd_ceil_at_s_di "s/d/"
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/s/di"
+test_prefix sd_ceil_at_s_di_slash "s/d/"
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/sdi"
+test_prefix sd_ceil_at_sdi "s/d/"
+
+export GIT_CEILING_DIRS="$TRASH_ROOT/sdi"
+test_prefix sd_ceil_at_sdi_slash "s/d/"
+
+
+test_done
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 7c2a8ba..22899c1 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -35,6 +35,7 @@ unset GIT_WORK_TREE
 unset GIT_EXTERNAL_DIFF
 unset GIT_INDEX_FILE
 unset GIT_OBJECT_DIRECTORY
+unset GIT_CEILING_DIRS
 unset SHA1_FILE_DIRECTORIES
 unset SHA1_FILE_DIRECTORY
 GIT_MERGE_VERBOSITY=5
-- 
1.5.4