Subject: [PATCH] Add support for GIT_CEILING_DIRS

From: David Reiss <[email-masked]>
Date: Mon, 21 Apr 2008 10:43:08 -0700

>>From f29e8a524f770596450dbbe338433f0dfc365377 Mon Sep 17 00:00:00 2001
From: David Reiss <[email-masked]>
Date: Sun, 20 Apr 2008 12:27:52 -0700
Subject: [PATCH] chdir after computing new cwd in check_repository_format_gently

In preparation for adding a check before the chdir.
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]>
---
 setup.c |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diff --git a/setup.c b/setup.c
index 3d2d958..a7b7f56 100644
--- a/setup.c
+++ b/setup.c
@@ -396,7 +396,6 @@ const char *setup_git_directory_gently(int *nongit_ok)
 			check_repository_format_gently(nongit_ok);
 			return NULL;
 		}
-		chdir("..");
 		do {
 			if (!offset) {
 				if (nongit_ok) {
@@ -408,6 +407,7 @@ const char *setup_git_directory_gently(int *nongit_ok)
 				die("Not a git repository");
 			}
 		} while (cwd[--offset] != '/');
+		chdir("..");
 	}
 
 	inside_git_dir = 0;
-- 
1.5.4

>>From 1543c81e75e7890475331bbae1de98ead45e5245 Mon Sep 17 00:00:00 2001
From: David Reiss <[email-masked]>
Date: Sun, 20 Apr 2008 22:54:11 -0700
Subject: [PATCH] Add support for GIT_CEILING_DIRS

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.

Signed-off-by: David Reiss <[email-masked]>
---
Just a bit of context about the motivation for this.  I use git in an
environment where homedirs are automounted and "ls /home/nonexistent"
takes about 9 seconds.  I use the git bash completion, which runs
"git help -a".  As of Git 1.5.5, this calls setup_git_directory_gently,
which searches /home/.git and /home/objects, which takes forever,
delaying all of my logins.  This patch lets me set
"GIT_CEILING_DIRS=/home", which makes this operation fast.  As a bonus,
the "git symbolic-ref HEAD" in my shell prompt now works quickly when
I'm in a non-git directory.

 Documentation/git.txt   |    8 ++++
 cache.h                 |    1 +
 setup.c                 |   67 +++++++++++++++++++++++++-----
 t/t1503-ceiling-dirs.sh |  104 +++++++++++++++++++++++++++++++++++++++++++++++
 t/test-lib.sh           |    1 +
 5 files changed, 170 insertions(+), 11 deletions(-)
 create mode 100755 t/t1503-ceiling-dirs.sh

diff --git a/Documentation/git.txt b/Documentation/git.txt
index 336fe99..cc52f6e 100644
--- a/Documentation/git.txt
+++ b/Documentation/git.txt
@@ -406,6 +406,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 50b28fa..e5e3dba 100644
--- a/cache.h
+++ b/cache.h
@@ -295,6 +295,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 a7b7f56..2f95d68 100644
--- a/setup.c
+++ b/setup.c
@@ -321,9 +321,10 @@ static int check_repository_format_gently(int *nongit_ok)
 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;
-	int len, offset;
+	int len, offset, max_offset = -1;
 
 	/*
 	 * Let's assume that we are in a git repository.
@@ -375,6 +376,37 @@ const char *setup_git_directory_gently(int *nongit_ok)
 	if (!getcwd(cwd, sizeof(cwd)-1))
 		die("Unable to read current working directory");
 
+	// Compute max_offset based on GIT_CEILING_DIRS.
+	if (env_ceiling_dirs) {
+		char *ceils, *ceil, *colon;
+		ceil = ceils = xstrdup(env_ceiling_dirs);
+		for (;;) {
+			int len;
+
+			if ((colon = strchr(ceil, ':')))
+				*colon = '\0';
+			len = strlen(ceil);
+
+			// "" would otherwise be treated like "/".
+			if (len) {
+				// Trim trailing slashes.
+				while (len && ceil[len-1] == '/')
+					ceil[--len] = '\0';
+
+				if (!strncmp(cwd, ceil, len) &&
+				    cwd[len] == '/' &&
+				    len > max_offset) {
+					max_offset = len;
+				}
+			}
+
+			if (!colon)
+				break;
+			ceil = colon + 1;
+		}
+		free(ceils);
+	}
+
 	/*
 	 * Test in the following order (relative to the cwd):
 	 * - .git/
@@ -386,6 +418,7 @@ const char *setup_git_directory_gently(int *nongit_ok)
 	 */
 	offset = len = strlen(cwd);
 	for (;;) {
+		// Check the current directory (.git first).
 		if (is_git_directory(DEFAULT_GIT_DIR_ENVIRONMENT))
 			break;
 		if (is_git_directory(".")) {
@@ -396,17 +429,29 @@ const char *setup_git_directory_gently(int *nongit_ok)
 			check_repository_format_gently(nongit_ok);
 			return NULL;
 		}
-		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");
+
+		// Did we just check the root dir?
+		if (!offset) {
+		not_a_repo:
+			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");
+		}
+
+		while (cwd[--offset] != '/') {
+			assert(offset > 0);
+		}
+
+		// Don't chdir into the ceiling.
+		if (offset <= max_offset) {
+			assert(offset == max_offset);
+			goto not_a_repo;
+		}
+
 		chdir("..");
 	}
 
diff --git a/t/t1503-ceiling-dirs.sh b/t/t1503-ceiling-dirs.sh
new file mode 100755
index 0000000..d4eaa13
--- /dev/null
+++ b/t/t1503-ceiling-dirs.sh
@@ -0,0 +1,104 @@
+#!/bin/sh
+
+test_description='test GIT_CEILING_DIRS'
+. ./test-lib.sh
+
+test_prefix() {
+	test_expect_success "$1" \
+	"test '$2' = \"\$(git rev-parse --show-prefix)\""
+	shift
+	[ $# -eq 0 ] && return
+}
+
+test_fail() {
+	test_expect_code 128 "$1: prefix" \
+	"git rev-parse --show-prefix"
+	shift
+	[ $# -eq 0 ] && return
+}
+
+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/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 ""
+
+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