Skip to contentSkip to conversation

Add support for GIT_CEILING_DIRS

PATCH In review David Reiss 4 participants 20 messages Jun 15, 2016 Documentation

lore
Share & tools
lore
#0 lore raw

PATCH v3 1/1 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.

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.

---
 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
Documentation/git.txt +8 -0
index 6f445b1..8aea331 100644
@@ -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'::
cache.h +1 -0
index 9cee9a5..8300acc 100644
@@ -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]"
setup.c +115 -12

155 lines not on this page · raw message

t/t1504-ceiling-dirs.sh new file +156 -0

156 lines not on this page · raw message

t/test-lib.sh +1 -0
index 7c2a8ba..22899c1 100644
@@ -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
Signature · 1 line
1.5.4
#1 lore raw
Quoted 3 lines · David Reiss

+ * .... Paths must
+ * be in a canonical form: empty components, or "." or ".." components
+ * are not allowed. prefix_list may be null, which is like "".

The caller starts from cwd[] and chomps, so you can safely assume that it would not feed anything problematic. But prefix_list comes from user's environment, and it is easy to make mistakes like doubled slashes (which you seem to take care) and also is tempting to use ".." when specifying the ceiling (e.g. "CEIL=$HOME/.."). Perhaps canonicalizing the ceiling would make this easier to use for end users?

setup.c const char *read_gitfile_gently(const char *path) · line 452

Quoted 5 lines
setup.c +4 -0
+#if 0
+static void test_longest_ancestor_length()
+{
...
+	assert(longest_ancestor_length("/foo/bar", "/:/bar/"      ) ==  0);

No test for nonsense/invalid input, like "::/foo" for prefix_list?

Quoted 19 lines
t/t1504-ceiling-dirs.sh new file +13 -0
new file mode 100755
index 0000000..091baad
@@ -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"

t/t1504-ceiling-dirs.sh line 23

Quoted 10 lines
t/t1504-ceiling-dirs.sh new file +10 -0
+}
+
+TRASH_ROOT="$(pwd)"
+ROOT_PARENT=$(dirname "$TRASH_ROOT")
+
+
+unset GIT_CEILING_DIRS
+test_prefix no_ceil ""
+
+export GIT_CEILING_DIRS=""

Portability. Instead write this as two separate statements, please.

VAR=val
export VAR

Quoted 12 lines
t/t1504-ceiling-dirs.sh new file +11 -0
+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 ""
+
...

t/t1504-ceiling-dirs.sh line 96

Quoted 10 lines
t/t1504-ceiling-dirs.sh new file +10 -0
+
+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

You may also check stuff like "sub//dir" and "::sub/dir/".

How well would this colon separated list work with msys folks?

#2 lore raw

Hi,

Quoted 6 lines · Junio C Hamano
Quoted 3 lines · David Reiss

+ * .... Paths must
+ * be in a canonical form: empty components, or "." or ".." components
+ * are not allowed. prefix_list may be null, which is like "".

Quoted 6 lines

The caller starts from cwd[] and chomps, so you can safely assume that it would not feed anything problematic. But prefix_list comes from user's environment, and it is easy to make mistakes like doubled slashes (which you seem to take care) and also is tempting to use ".." when specifying the ceiling (e.g. "CEIL=$HOME/.."). Perhaps canonicalizing the ceiling would make this easier to use for end users?

Is this not going too far? I mean, CEILING_DIRECTORIES is already a very special case.

Quoted 1 line

How well would this colon separated list work with msys folks?

Not well at all. At least for the moment, I think setting this variable would fail (since it would be rewritten into a Windows-PATH-style string). Though honestly, I have no idea what getcwd() does on MinGW.

Steffen is working on that rewriting stuff, so maybe it will be a non-issue, eventually.

Ciao,
Dscho

#3 lore raw
Quoted 8 lines · Johannes Schindelin
Quoted 2 lines · Junio C Hamano

David Reiss <[email protected]> writes:
How well would this colon separated list work with msys folks?

Not well at all. At least for the moment, I think setting this variable would fail (since it would be rewritten into a Windows-PATH-style string). Though honestly, I have no idea what getcwd() does on MinGW.

We would have to use ';' instead of ':' in longest_prefix(). Then it will work automatically. Except that it does not ignore case in path names and drive letters. Hence, the user must set this environment variable to the precise path name(s).

Quoted 2 lines

Steffen is working on that rewriting stuff, so maybe it will be a non-issue, eventually.

This will not matter. When we change the code to use is_path_separator() instead of hard-coded c == ':', then it will work automatically. We do that already for GIT_ALTERNATE_OBJECT_DIRECTORIES.

-- Hannes

#4 lore raw

Hi,

Quoted 6 lines · Johannes Sixt
Quoted 5 lines · Johannes Schindelin
Quoted 2 lines · Junio C Hamano

David Reiss <[email protected]> writes:
How well would this colon separated list work with msys folks?

Quoted 8 lines
Quoted 3 lines

Not well at all. At least for the moment, I think setting this variable would fail (since it would be rewritten into a Windows-PATH-style string). Though honestly, I have no idea what getcwd() does on MinGW.

We would have to use ';' instead of ':' in longest_prefix(). Then it will work automatically. Except that it does not ignore case in path names and drive letters. Hence, the user must set this environment variable to the precise path name(s).

Yes, good point.

Quoted 6 lines
Quoted 2 lines

Steffen is working on that rewriting stuff, so maybe it will be a non-issue, eventually.

This will not matter. When we change the code to use is_path_separator() instead of hard-coded c == ':', then it will work automatically. We do that already for GIT_ALTERNATE_OBJECT_DIRECTORIES.

You are right, of course.

Ciao,
Dscho

#5 lore raw
In reply to David Reiss

Hi,

Quoted 3 lines · David Reiss
 cache.h                 |    1 +
 setup.c                 |  127 ++++++++++++++++++++++++++++++++++----
 t/t1504-ceiling-dirs.sh |  156 +++++++++++++++++++++++++++++++++++++++++++++++

By now, I strongly believe that these changes are too large. I am convinced that what you desire can be expressed much simpler, and thus less error-prone.

Also, I think that your test cases are too extensive. While it is usually good to have exhaustive tests, running them takes time. And if it takes so much time that hardly anybody bothers with running the test suite, there are _too_ many tests.

Quoted 52 lines
setup.c +44 -0
index b8fd476..fdcfae1 100644
@@ -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;

setup.c const char *read_gitfile_gently(const char *path) · line 409

Quoted 10 lines
setup.c +10 -0
+			}
+		}
+
+		if (!colon)
+			break;
+		ceil = colon + 1;
+	}
+
+	return max_len;
+}

You know, I wonder why I even bothered writing those responses, if you just ignore them.

Quoted 34 lines
setup.c +34 -0
+#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);

setup.c const char *read_gitfile_gently(const char *path) · line 454

Quoted 10 lines
setup.c +10 -0
+	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

This has _no_ place in the Git source code. Have you looked around, and found similar dead code? No? That's because Git's source code is not a graveyard of useless code bits, but it is a collection of elegant code. Mostly, at least.

Instead of wasting my time further, I will try to come up with a better implementation, as is the way of Open Source.

Ciao,
Dscho

#6 lore raw
Quoted 3 lines

By now, I strongly believe that these changes are too large. I am convinced that what you desire can be expressed much simpler, and thus less error-prone.

Most of the code is in the one function to parse out the colon-separated environment variable value and compute the longest directory prefix. I'm not convinced this can be made much simpler. (Using strtok_r could help, but would require an allocation.) Most of the rest of the changes are test code and indentation.

Quoted 4 lines

Also, I think that your test cases are too extensive. While it is usually good to have exhaustive tests, running them takes time. And if it takes so much time that hardly anybody bothers with running the test suite, there are _too_ many tests.

I am more than happy to remove most of them, leaving only basic sanity checks. However, I would prefer to leave them in but comment them out, so that if I or someone else wants to modify this code later, they would be able to run a more extensive test suite. I'll submit a modified patch with this change.

Quoted 2 lines

You know, I wonder why I even bothered writing those responses, if you just ignore them.

I must say that I am very confused. I thought I followed all of your responses to the letter. Could you please be more specific about the ones I missed? I'm happy to make further changes.

Quoted 4 lines

This has _no_ place in the Git source code. Have you looked around, and found similar dead code? No? That's because Git's source code is not a graveyard of useless code bits, but it is a collection of elegant code. Mostly, at least.

As I stated in "PATCH v2", I was unsure what the convention was for unit tests like this. Most of the git code is (obviously) functional tests, but it is impossible to test how this code would behave with a git directory under "/" using a functional test, unless it was run as root. Someone just pointed out to me that there are some C-based tests (like test-sha1) that are run from "make test". I guess I can move the test function to a new one of those, but it will require making longest_ancestor_length extern.

Quoted 2 lines

Instead of wasting my time further, I will try to come up with a better implementation, as is the way of Open Source.

I am sorry if this has wasted your time. I really have been trying to incorporate your feedback into my patch, and the code has definitely improved as a result. However, my main goal is simply to get this feature working (I have already patched it into my own git build, and it has saved me a lot of time), so if you come up with a better implementation, that would be great!

--David

#7 lore raw

PATCH 1/1 Add support for GIT_CEILING_DIRECTORIES

In reply to Johannes Schindelin

In certain setups, trying to access a non-existing .git/ can take quite some time, for example when the directory is an automount directory.

Allow the user to specify directories where Git should stop looking for a .git/ directory: GIT_CEILING_DIRECTORIES, if set, is expected to be a colon delimited list of such barrier directories.

Note: if GIT_CEILING_DIRECTORIES=/a/b and your current working directory is /a, Git will _not_ stop looking.

Note2: you must not specify the directories with trailing slashes.

Initial implementation by David Reiss.

---

Show me the bugs!

I just checked: the "/" issue you were referring to is most likely the fact that "git rev-parse --git-dir" would return "//.git" instead of "/.git" (if that is the appropriate GIT_DIR).

This is the original behavior (without this patch), and IMO a separate issue, which might not even need fixing.

 Documentation/git.txt          |    6 +++++
 cache.h                        |    2 +
 path.c                         |   19 ++++++++++++++++
 setup.c                        |   11 +++++++-
 t/t1504-ceiling-directories.sh |   46 ++++++++++++++++++++++++++++++++++++++++
 t/test-lib.sh                  |    1 +
 6 files changed, 83 insertions(+), 2 deletions(-)
 create mode 100644 t/t1504-ceiling-directories.sh
Documentation/git.txt +6 -0
index adcd3e0..a12d1f8 100644
@@ -415,6 +415,12 @@ 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'::
+	If set (to a colon delimited list of absolute directories), Git
+	will refuse to look for the .git/ directory further when hitting
+	one of those directories (otherwise it would traverse the parent
+	directories until hitting the root directory).
+
 git Commits
 ~~~~~~~~~~~
 'GIT_AUTHOR_NAME'::
cache.h +2 -0
index a8638b1..c31b4c7 100644
@@ -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_DIRECTORIES_ENVIRONMENT "GIT_CEILING_DIRECTORIES"
 #define GITATTRIBUTES_FILE ".gitattributes"
 #define INFOATTRIBUTES_FILE "info/attributes"
 #define ATTRIBUTE_MACRO_PREFIX "[attr]"
@@ -522,6 +523,7 @@ static inline int is_absolute_path(const char *path)
 	return path[0] == '/';
 }
 const char *make_absolute_path(const char *path);
+int longest_prefix(const char *path, const char *prefix_list);
 
 /* Read and unpack a sha1 file into memory, write memory to a sha1 file */
 extern int sha1_object_info(const unsigned char *, unsigned long *);
path.c +19 -0
index b7c24a2..c0d7364 100644
@@ -357,3 +357,22 @@ const char *make_absolute_path(const char *path)
 
 	return buf;
 }
+
+int longest_prefix(const char *path, const char *prefix_list)
+{
+	int max_length = 0, length = 0, i;
+
+	for (i = 0; prefix_list[i]; i++)
+		if (prefix_list[i] == ':') {
+			if (length > max_length)
+				max_length = length;
+			length = 0;
+		}
+		else if (length >= 0) {
+			if (prefix_list[i] == path[length])
+				length++;
+			else
+				length = -1;
+		}
+	return max_length > length ? max_length : length;
+}
setup.c +9 -2
index 9e9a2b1..cece3e4 100644
@@ -365,10 +365,13 @@ const char *read_gitfile_gently(const char *path)
 const char *setup_git_directory_gently(int *nongit_ok)
 {
 	const char *work_tree_env = getenv(GIT_WORK_TREE_ENVIRONMENT);
+	const char *ceiling_directories =
+		getenv(CEILING_DIRECTORIES_ENVIRONMENT);
 	static char cwd[PATH_MAX+1];
 	const char *gitdirenv;
 	const char *gitfile_dir;
 	int len, offset;
+	int min_offset = 0;
 
 	/*
 	 * Let's assume that we are in a git repository.
@@ -422,6 +425,9 @@ const char *setup_git_directory_gently(int *nongit_ok)
 	if (!getcwd(cwd, sizeof(cwd)-1))
 		die("Unable to read current working directory");
 
+	if (ceiling_directories)
+		min_offset = longest_prefix(cwd, ceiling_directories);
+
 	/*
 	 * Test in the following order (relative to the cwd):
 	 * - .git (file containing "gitdir: <path>")
@@ -453,7 +459,7 @@ const char *setup_git_directory_gently(int *nongit_ok)
 		}
 		chdir("..");
 		do {
-			if (!offset) {
+			if (offset <= min_offset) {
 				if (nongit_ok) {
 					if (chdir(cwd))
 						die("Cannot come back to cwd");
@@ -462,7 +468,8 @@ const char *setup_git_directory_gently(int *nongit_ok)
 				}
 				die("Not a git repository");
 			}
-		} while (cwd[--offset] != '/');
+		} while (offset > min_offset &&
+				--offset >=0 && cwd[offset] != '/');
 	}
 
 	inside_git_dir = 0;
t/t1504-ceiling-directories.sh new file +46 -0

46 lines not on this page · raw message

t/test-lib.sh +1 -0
index 5002fb0..c3a3167 100644
@@ -35,6 +35,7 @@ unset GIT_WORK_TREE
 unset GIT_EXTERNAL_DIFF
 unset GIT_INDEX_FILE
 unset GIT_OBJECT_DIRECTORY
+unset GIT_CEILING_DIRECTORIES
 unset SHA1_FILE_DIRECTORIES
 unset SHA1_FILE_DIRECTORY
 GIT_MERGE_VERBOSITY=5
Signature · 1 line
1.5.5.1.425.g5f464
#8 lore raw

Re: [PATCH] Add support for GIT_CEILING_DIRECTORIES

longest_prefix is just a textual check. It doesn't verify that the prefix is actually a full directory component of the cwd. For example...

dreiss@dreiss-vmware:/tmp$ mkdir test
dreiss@dreiss-vmware:/tmp$ cd test
dreiss@dreiss-vmware:/tmp/test$ git init
Initialized empty Git repository in .git/
dreiss@dreiss-vmware:master:test$ mkdir abcd
dreiss@dreiss-vmware:master:test$ cd abcd
dreiss@dreiss-vmware:master:test/abcd$ git rev-parse --show-prefix
abcd/
dreiss@dreiss-vmware:master:test/abcd$ ~/git-ceil/git rev-parse --show-prefix
abcd/
dreiss@dreiss-vmware:master:test/abcd$ GIT_CEILING_DIRECTORIES=/tmp/test/ab ~/git-ceil/git rev-parse --show-prefix
fatal: Not a git repository
dreiss@dreiss-vmware:master:test/abcd:128$ 

"/tmp/test/ab" is a textual prefix of the cwd, but it should not prevent "/tmp/test/.git" from being discovered. For what it's worth, my test cases check this behavior. Ironically, I think this could be fixed by requiring the ceiling directories to have trailing slashes.

Also, I think it is better to move the 'chdir("..")' after the do loop, so that git won't even chdir up into the ceiling directory. This actually doesn't matter to me, but I figured that it might be nice for someone.

Finally, just a small thing. The documentation still says "GIT_CEILING_DIRS".

--David

Quoted 217 lines · Johannes Schindelin

In certain setups, trying to access a non-existing .git/ can take quite some time, for example when the directory is an automount directory.

Allow the user to specify directories where Git should stop looking for a .git/ directory: GIT_CEILING_DIRECTORIES, if set, is expected to be a colon delimited list of such barrier directories.

Note: if GIT_CEILING_DIRECTORIES=/a/b and your current working directory is /a, Git will _not_ stop looking.

Note2: you must not specify the directories with trailing slashes.

Initial implementation by David Reiss.

---

Show me the bugs!

I just checked: the "/" issue you were referring to is most likely the fact that "git rev-parse --git-dir" would return "//.git" instead of "/.git" (if that is the appropriate GIT_DIR).

This is the original behavior (without this patch), and IMO a separate issue, which might not even need fixing.

 Documentation/git.txt          |    6 +++++
 cache.h                        |    2 +
 path.c                         |   19 ++++++++++++++++
 setup.c                        |   11 +++++++-
 t/t1504-ceiling-directories.sh |   46
++++++++++++++++++++++++++++++++++++++++
 t/test-lib.sh                  |    1 +
 6 files changed, 83 insertions(+), 2 deletions(-)
 create mode 100644 t/t1504-ceiling-directories.sh
Documentation/git.txt +6 -0
index adcd3e0..a12d1f8 100644
@@ -415,6 +415,12 @@ 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'::
+       If set (to a colon delimited list of absolute directories), Git
+       will refuse to look for the .git/ directory further when hitting
+       one of those directories (otherwise it would traverse the parent
+       directories until hitting the root directory).
+
 git Commits
 ~~~~~~~~~~~
 'GIT_AUTHOR_NAME'::
cache.h +1 -0
index a8638b1..c31b4c7 100644
@@ -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_DIRECTORIES_ENVIRONMENT "GIT_CEILING_DIRECTORIES"
 #define GITATTRIBUTES_FILE ".gitattributes"
 #define INFOATTRIBUTES_FILE "info/attributes"

#define ATTRIBUTE_MACRO_PREFIX "[attr]"

cache.h +1 -0
@@ -522,6 +523,7 @@ static inline int is_absolute_path(const char *path)
        return path[0] == '/';
 }
 const char *make_absolute_path(const char *path);
+int longest_prefix(const char *path, const char *prefix_list);
 
 /* Read and unpack a sha1 file into memory, write memory to a sha1 file */
 extern int sha1_object_info(const unsigned char *, unsigned long *);
path.c +19 -0
index b7c24a2..c0d7364 100644
@@ -357,3 +357,22 @@ const char *make_absolute_path(const char *path)
 
        return buf;
 }
+
+int longest_prefix(const char *path, const char *prefix_list)
+{
+       int max_length = 0, length = 0, i;
+
+       for (i = 0; prefix_list[i]; i++)
+               if (prefix_list[i] == ':') {
+                       if (length > max_length)
+                               max_length = length;
+                       length = 0;
+               }
+               else if (length >= 0) {
+                       if (prefix_list[i] == path[length])
+                               length++;
+                       else
+                               length = -1;
+               }
+       return max_length > length ? max_length : length;
+}
setup.c +7 -1
index 9e9a2b1..cece3e4 100644
@@ -365,10 +365,13 @@ const char *read_gitfile_gently(const char *path)
 const char *setup_git_directory_gently(int *nongit_ok)
 {
        const char *work_tree_env = getenv(GIT_WORK_TREE_ENVIRONMENT);
+       const char *ceiling_directories =
+               getenv(CEILING_DIRECTORIES_ENVIRONMENT);
        static char cwd[PATH_MAX+1];
        const char *gitdirenv;
        const char *gitfile_dir;
        int len, offset;
+       int min_offset = 0;
 
        /*
         * Let's assume that we are in a git repository.
@@ -422,6 +425,9 @@ const char *setup_git_directory_gently(int *nongit_ok)
        if (!getcwd(cwd, sizeof(cwd)-1))
                die("Unable to read current working directory");
 
+       if (ceiling_directories)
+               min_offset = longest_prefix(cwd, ceiling_directories);
+
        /*
         * Test in the following order (relative to the cwd):
         * - .git (file containing "gitdir: <path>")
@@ -453,7 +459,7 @@ const char *setup_git_directory_gently(int *nongit_ok)
                }
                chdir("..");
                do {
-                       if (!offset) {
+                       if (offset <= min_offset) {
                                if (nongit_ok) {
                                        if (chdir(cwd))
                                                die("Cannot come back to

cwd");

setup.c +2 -1
@@ -462,7 +468,8 @@ const char *setup_git_directory_gently(int *nongit_ok)
                                }
                                die("Not a git repository");
                        }
-               } while (cwd[--offset] != '/');
+               } while (offset > min_offset &&
+                               --offset >=0 && cwd[offset] != '/');
        }
 
        inside_git_dir = 0;
t/t1504-ceiling-directories.sh new file +46 -0
new file mode 100644
index 0000000..1d8ef0b
@@ -0,0 +1,46 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Johannes E. Schindelin
+#
+
+test_description='test limiting with GIT_CEILING_DIRECTORIES'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+
+       CWD="$(pwd -P)" &&
+       mkdir subdir
+
+'
+
+test_expect_success 'without GIT_CEILING_DIRECTORIES' '
+
+       test .git = "$(git rev-parse --git-dir)" &&
+       (cd subdir && git rev-parse --git-dir) &&
+       echo "$CWD" &&
+       test "$CWD/.git" = "$(cd subdir && git rev-parse --git-dir)"
+
+'
+
+test_expect_success 'with non-matching ceiling directory' '
+
+       test "$(GIT_CEILING_DIRECTORIES="$CWD/X" \
+               git rev-parse --git-dir)" = .git
+
+'
+
+test_expect_success 'with matching ceiling directories' '
+
+       GIT_CEILING_DIRECTORIES="$CWD/X:$CWD/subdir" &&
+       export GIT_CEILING_DIRECTORIES &&
+       (cd subdir && test_must_fail git rev-parse --git-dir) &&
+       git rev-parse --git-dir &&
+       GIT_CEILING_DIRECTORIES="$CWD/subdir:$CWD/X" &&
+       export GIT_CEILING_DIRECTORIES &&
+       (cd subdir && test_must_fail git rev-parse --git-dir) &&
+       git rev-parse --git-dir
+
+'
+
+test_done
t/test-lib.sh +1 -1
index 5002fb0..c3a3167 100644
@@ -35,6 +35,7 @@ unset GIT_WORK_TREE
 unset GIT_EXTERNAL_DIFF
 unset GIT_INDEX_FILE
 unset GIT_OBJECT_DIRECTORY
+unset GIT_CEILING_DIRECTORIES
 unset SHA1_FILE_DIRECTORIES
 unset SHA1_FILE_DIRECTORY
 GIT_MERGE_VERBOSITY=5
--

1.5.5.1.425.g5f464

#9 lore raw

Re: [PATCH] Add support for GIT_CEILING_DIRECTORIES

Hi,

Quoted 2 lines · David Reiss

longest_prefix is just a textual check. It doesn't verify that the prefix is actually a full directory component of the cwd.

Okay.

Quoted 4 lines

Also, I think it is better to move the 'chdir("..")' after the do loop, so that git won't even chdir up into the ceiling directory. This actually doesn't matter to me, but I figured that it might be nice for someone.

I'd rather go with the minimal diff, unless there is a good reason to change it.

Quoted 2 lines

Finally, just a small thing. The documentation still says "GIT_CEILING_DIRS".

Okay.

How about this on top (still pretty simple):

---

 Documentation/git.txt          |    2 +-
 path.c                         |   10 ++++++++--
 t/t1504-ceiling-directories.sh |    8 ++++++++
 3 files changed, 17 insertions(+), 3 deletions(-)
Documentation/git.txt +1 -1
index a12d1f8..e4413bf 100644
@@ -415,7 +415,7 @@ 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'::
+'GIT_CEILING_DIRECTORIES'::
 	If set (to a colon delimited list of absolute directories), Git
 	will refuse to look for the .git/ directory further when hitting
 	one of those directories (otherwise it would traverse the parent
path.c +8 -2
index c0d7364..a097ecc 100644
@@ -358,13 +358,18 @@ const char *make_absolute_path(const char *path)
 	return buf;
 }
 
+static int is_separator(char c)
+{
+	return !c || c == '/';
+}
+
 int longest_prefix(const char *path, const char *prefix_list)
 {
 	int max_length = 0, length = 0, i;
 
 	for (i = 0; prefix_list[i]; i++)
 		if (prefix_list[i] == ':') {
-			if (length > max_length)
+			if (length > max_length && is_separator(path[length]))
 				max_length = length;
 			length = 0;
 		}
@@ -374,5 +379,6 @@ int longest_prefix(const char *path, const char *prefix_list)
 			else
 				length = -1;
 		}
-	return max_length > length ? max_length : length;
+	return max_length > length || !is_separator(path[length]) ?
+		max_length : length;
 }
t/t1504-ceiling-directories.sh +8 -0
index 1d8ef0b..6c8757d 100644
@@ -43,4 +43,12 @@ test_expect_success 'with matching ceiling directories' '
 
 '
 
+test_expect_success 'with non-directory prefix' '
+
+	GIT_CEILING_DIRECTORIES="$CWD/sub" &&
+	export GIT_CEILING_DIRECTORIES &&
+	(cd subdir && git rev-parse --git-dir)
+
+'
+
 test_done
#10 lore raw

Re: [PATCH] Add support for GIT_CEILING_DIRECTORIES

This meets my needs.

Junio, if you'd like, I can incorporate your suggestions of normalizing paths internally and testing more corner cases. But if you just want to take this version, I'll be fine.

--David

Quoted 96 lines · Johannes Schindelin

Hi,

Quoted 2 lines · David Reiss

longest_prefix is just a textual check. It doesn't verify that the prefix is actually a full directory component of the cwd.

Okay.

Quoted 4 lines

Also, I think it is better to move the 'chdir("..")' after the do loop, so that git won't even chdir up into the ceiling directory. This actually doesn't matter to me, but I figured that it might be nice for someone.

I'd rather go with the minimal diff, unless there is a good reason to change it.

Quoted 2 lines

Finally, just a small thing. The documentation still says "GIT_CEILING_DIRS".

Okay.

How about this on top (still pretty simple):

---

 Documentation/git.txt          |    2 +-
 path.c                         |   10 ++++++++--
 t/t1504-ceiling-directories.sh |    8 ++++++++
 3 files changed, 17 insertions(+), 3 deletions(-)
Documentation/git.txt +1 -1
index a12d1f8..e4413bf 100644
@@ -415,7 +415,7 @@ 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'::
+'GIT_CEILING_DIRECTORIES'::
        If set (to a colon delimited list of absolute directories), Git
        will refuse to look for the .git/ directory further when hitting
        one of those directories (otherwise it would traverse the parent
path.c +6 -1
index c0d7364..a097ecc 100644
@@ -358,13 +358,18 @@ const char *make_absolute_path(const char *path)
        return buf;
 }
 
+static int is_separator(char c)
+{
+       return !c || c == '/';
+}
+
 int longest_prefix(const char *path, const char *prefix_list)
 {
        int max_length = 0, length = 0, i;
 
        for (i = 0; prefix_list[i]; i++)
                if (prefix_list[i] == ':') {
-                       if (length > max_length)
+                       if (length > max_length &&
 is_separator(path[length]))
                                max_length = length;
                        length = 0;
                }
path.c +2 -1
@@ -374,5 +379,6 @@ int longest_prefix(const char *path, const char
 *prefix_list)
                        else
                                length = -1;
                }
-       return max_length > length ? max_length : length;
+       return max_length > length || !is_separator(path[length]) ?
+               max_length : length;

}

t/t1504-ceiling-directories.sh +8 -0
index 1d8ef0b..6c8757d 100644
@@ -43,4 +43,12 @@ test_expect_success 'with matching ceiling directories' '
 
 '
 
+test_expect_success 'with non-directory prefix' '
+
+       GIT_CEILING_DIRECTORIES="$CWD/sub" &&
+       export GIT_CEILING_DIRECTORIES &&
+       (cd subdir && git rev-parse --git-dir)
+
+'
+
 test_done
#11 lore raw

PATCH 1/1 Add support for GIT_CEILING_DIRECTORIES

In certain setups, trying to access a non-existing .git/ can take quite some time, for example when the directory is an automount directory.

Allow the user to specify directories where Git would stop looking for a .git/ directory: GIT_CEILING_DIRECTORIES, if set, is expected to be a colon delimited list of such barrier directories.

Note: if GIT_CEILING_DIRECTORIES=/a/b and your current working directory is /a, Git will _not_ stop looking.

Note2: you must not specify the directories with trailing slashes.

Initial implementation by David Reiss.

---

On Thu, 15 May 2008, David Reiss wrote:

> This meets my needs.
>
> Junio, if you'd like, I can incorporate your suggestions of > normalizing paths internally and testing more corner cases. But > if you just want to take this version, I'll be fine.

For your pleasure, the combined patch of my work (so that it is easier to hack on normalizing paths, if you still want that -- you might want to look at get_pathspec() and its history in that case).

 Documentation/git.txt          |    6 ++++
 cache.h                        |    2 +
 path.c                         |   25 ++++++++++++++++++
 setup.c                        |   11 ++++++-
 t/t1504-ceiling-directories.sh |   54 ++++++++++++++++++++++++++++++++++++++++
 t/test-lib.sh                  |    1 +
 6 files changed, 97 insertions(+), 2 deletions(-)
 create mode 100644 t/t1504-ceiling-directories.sh
Documentation/git.txt +6 -0
index adcd3e0..e4413bf 100644
@@ -415,6 +415,12 @@ 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_DIRECTORIES'::
+	If set (to a colon delimited list of absolute directories), Git
+	will refuse to look for the .git/ directory further when hitting
+	one of those directories (otherwise it would traverse the parent
+	directories until hitting the root directory).
+
 git Commits
 ~~~~~~~~~~~
 'GIT_AUTHOR_NAME'::
cache.h +2 -0
index a8638b1..c31b4c7 100644
@@ -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_DIRECTORIES_ENVIRONMENT "GIT_CEILING_DIRECTORIES"
 #define GITATTRIBUTES_FILE ".gitattributes"
 #define INFOATTRIBUTES_FILE "info/attributes"
 #define ATTRIBUTE_MACRO_PREFIX "[attr]"
@@ -522,6 +523,7 @@ static inline int is_absolute_path(const char *path)
 	return path[0] == '/';
 }
 const char *make_absolute_path(const char *path);
+int longest_prefix(const char *path, const char *prefix_list);
 
 /* Read and unpack a sha1 file into memory, write memory to a sha1 file */
 extern int sha1_object_info(const unsigned char *, unsigned long *);
path.c +25 -0
index b7c24a2..a097ecc 100644
@@ -357,3 +357,28 @@ const char *make_absolute_path(const char *path)
 
 	return buf;
 }
+
+static int is_separator(char c)
+{
+	return !c || c == '/';
+}
+
+int longest_prefix(const char *path, const char *prefix_list)
+{
+	int max_length = 0, length = 0, i;
+
+	for (i = 0; prefix_list[i]; i++)
+		if (prefix_list[i] == ':') {
+			if (length > max_length && is_separator(path[length]))
+				max_length = length;
+			length = 0;
+		}
+		else if (length >= 0) {
+			if (prefix_list[i] == path[length])
+				length++;
+			else
+				length = -1;
+		}
+	return max_length > length || !is_separator(path[length]) ?
+		max_length : length;
+}
setup.c +9 -2
index 9e9a2b1..cece3e4 100644
@@ -365,10 +365,13 @@ const char *read_gitfile_gently(const char *path)
 const char *setup_git_directory_gently(int *nongit_ok)
 {
 	const char *work_tree_env = getenv(GIT_WORK_TREE_ENVIRONMENT);
+	const char *ceiling_directories =
+		getenv(CEILING_DIRECTORIES_ENVIRONMENT);
 	static char cwd[PATH_MAX+1];
 	const char *gitdirenv;
 	const char *gitfile_dir;
 	int len, offset;
+	int min_offset = 0;
 
 	/*
 	 * Let's assume that we are in a git repository.
@@ -422,6 +425,9 @@ const char *setup_git_directory_gently(int *nongit_ok)
 	if (!getcwd(cwd, sizeof(cwd)-1))
 		die("Unable to read current working directory");
 
+	if (ceiling_directories)
+		min_offset = longest_prefix(cwd, ceiling_directories);
+
 	/*
 	 * Test in the following order (relative to the cwd):
 	 * - .git (file containing "gitdir: <path>")
@@ -453,7 +459,7 @@ const char *setup_git_directory_gently(int *nongit_ok)
 		}
 		chdir("..");
 		do {
-			if (!offset) {
+			if (offset <= min_offset) {
 				if (nongit_ok) {
 					if (chdir(cwd))
 						die("Cannot come back to cwd");
@@ -462,7 +468,8 @@ const char *setup_git_directory_gently(int *nongit_ok)
 				}
 				die("Not a git repository");
 			}
-		} while (cwd[--offset] != '/');
+		} while (offset > min_offset &&
+				--offset >=0 && cwd[offset] != '/');
 	}
 
 	inside_git_dir = 0;
t/t1504-ceiling-directories.sh new file +54 -0

54 lines not on this page · raw message

t/test-lib.sh +1 -0
index 5002fb0..c3a3167 100644
@@ -35,6 +35,7 @@ unset GIT_WORK_TREE
 unset GIT_EXTERNAL_DIFF
 unset GIT_INDEX_FILE
 unset GIT_OBJECT_DIRECTORY
+unset GIT_CEILING_DIRECTORIES
 unset SHA1_FILE_DIRECTORIES
 unset SHA1_FILE_DIRECTORY
 GIT_MERGE_VERBOSITY=5
Signature · 1 line
1.5.5.1.425.g5f464.dirty
#12 lore raw

Re: [SQUASHED PATCH] Add support for GIT_CEILING_DIRECTORIES

Documentation/git.txt git so take care if using Cogito etc. · line 422

Quoted 5 lines · Johannes Schindelin

+'GIT_CEILING_DIRECTORIES'::

Documentation/git.txt +4 -0
+	If set (to a colon delimited list of absolute directories), Git
+	will refuse to look for the .git/ directory further when hitting
+	one of those directories (otherwise it would traverse the parent
+	directories until hitting the root directory).

Hmm.

Looking at the current implementation, this should be written as:

If set to a colon delimited list of absolute directories, and the current directory is in or below one of them, then these are the top-most directories in which Git will look for a .git/ directory (otherwise it would traverse the parent directories until hitting the root directory).

But from David's initial commit message:

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 implementation it will still look for a non-existing /home/.git, and, hence, should take a long time to complete.

David, does this really meet your needs?

-- Hannes

#13 lore raw

Re: [SQUASHED PATCH] Add support for GIT_CEILING_DIRECTORIES

Hi,

Quoted 9 lines · Johannes Sixt

Documentation/git.txt git so take care if using Cogito etc. · line 422

Quoted 5 lines · Johannes Schindelin

+'GIT_CEILING_DIRECTORIES'::

Documentation/git.txt +4 -0
+	If set (to a colon delimited list of absolute directories), Git
+	will refuse to look for the .git/ directory further when hitting
+	one of those directories (otherwise it would traverse the parent
+	directories until hitting the root directory).

Hmm.

Quoted 7 lines

Looking at the current implementation, this should be written as:

If set to a colon delimited list of absolute directories, and the current directory is in or below one of them, then these are the top-most directories in which Git will look for a .git/ directory (otherwise it would traverse the parent directories until hitting the root directory).

According to the test case

        GIT_CEILING_DIRECTORIES="$CWD/X:$CWD/subdir" &&
        export GIT_CEILING_DIRECTORIES &&
        (cd subdir && test_must_fail git rev-parse --git-dir)

this is not the case. If you have something like /bla/subdir and you are in /bla/subdir, it will not look for .git/.

Ciao,
Dscho "who leaves it as an exercise to the reader to see why the code does what it does"

#14 lore raw

Re: [SQUASHED PATCH] Add support for GIT_CEILING_DIRECTORIES

Quoted 20 lines · Johannes Schindelin

Hi,

Quoted 8 lines · Johannes Sixt

Documentation/git.txt git so take care if using Cogito etc. · line 422

Quoted 5 lines · Johannes Schindelin

+'GIT_CEILING_DIRECTORIES'::

Documentation/git.txt +4 -0
+	If set (to a colon delimited list of absolute directories), Git
+	will refuse to look for the .git/ directory further when hitting
+	one of those directories (otherwise it would traverse the parent
+	directories until hitting the root directory).

Hmm.

Quoted 7 lines

Looking at the current implementation, this should be written as:

If set to a colon delimited list of absolute directories, and the current directory is in or below one of them, then these are the top-most directories in which Git will look for a .git/ directory (otherwise it would traverse the parent directories until hitting the root directory).

Quoted 8 lines

According to the test case

        GIT_CEILING_DIRECTORIES="$CWD/X:$CWD/subdir" &&
        export GIT_CEILING_DIRECTORIES &&
        (cd subdir && test_must_fail git rev-parse --git-dir)

this is not the case. If you have something like /bla/subdir and you are in /bla/subdir, it will not look for .git/.

No, the test just shows that it does not *find* a subdir/.git/, but it does not show that it doesn't even *look* for it.

Let's first clarify the semantics of GIT_CEILING_DIRECTORIES before we start fixing something: Can a directory that is named in GIT_CEILING_DIRECTORIES be a git repository or not?

David's original motivation was that it cannot; in your implementation it can.

-- Hannes

#15 lore raw

Re: [SQUASHED PATCH] Add support for GIT_CEILING_DIRECTORIES

Sorry I missed this before. As you said, in Johannes's version the ceiling directories are the last directories we look in, whereas in my implementation, they are the first directories we do not look in. I made this choice because it makes more sense for me to set my ceiling to "/home", rather than "/home/dreiss", so it will work even if I am in another user's homedir.

There is also a difference in how they handle the case where the cwd is a ceiling directory, but I think it is worth sorting out the first issue first.

--David

Quoted 42 lines · Johannes Sixt
Quoted 20 lines · Johannes Schindelin

Hi,

Quoted 8 lines · Johannes Sixt

Documentation/git.txt git so take care if using Cogito etc. · line 422

Quoted 5 lines · Johannes Schindelin

+'GIT_CEILING_DIRECTORIES'::

Documentation/git.txt +4 -0
+   If set (to a colon delimited list of absolute directories), Git
+   will refuse to look for the .git/ directory further when hitting
+   one of those directories (otherwise it would traverse the parent
+   directories until hitting the root directory).

Hmm.

Quoted 7 lines

Looking at the current implementation, this should be written as:

If set to a colon delimited list of absolute directories, and the current directory is in or below one of them, then these are the top-most directories in which Git will look for a .git/ directory (otherwise it would traverse the parent directories until hitting the root directory).

Quoted 8 lines

According to the test case

        GIT_CEILING_DIRECTORIES="$CWD/X:$CWD/subdir" &&
        export GIT_CEILING_DIRECTORIES &&
        (cd subdir && test_must_fail git rev-parse --git-dir)

this is not the case. If you have something like /bla/subdir and you are in /bla/subdir, it will not look for .git/.

No, the test just shows that it does not *find* a subdir/.git/, but it does not show that it doesn't even *look* for it.

Let's first clarify the semantics of GIT_CEILING_DIRECTORIES before we start fixing something: Can a directory that is named in GIT_CEILING_DIRECTORIES be a git repository or not?

David's original motivation was that it cannot; in your implementation it can.

-- Hannes

#16 lore raw

Re: [SQUASHED PATCH] Add support for GIT_CEILING_DIRECTORIES

Hi,

Quoted 10 lines · David Reiss

Sorry I missed this before. As you said, in Johannes's version the ceiling directories are the last directories we look in, whereas in my implementation, they are the first directories we do not look in. I made this choice because it makes more sense for me to set my ceiling to "/home", rather than "/home/dreiss", so it will work even if I am in another user's homedir.

There is also a difference in how they handle the case where the cwd is a ceiling directory, but I think it is worth sorting out the first issue first.

Please do not top-post.

This is the interdiff to the last squashed patch:

-- snip --

setup.c +3 -0
index cece3e4..2f7a17a 100644
@@ -441,6 +441,8 @@ const char *setup_git_directory_gently(int *nongit_ok)
 	 */
 	offset = len = strlen(cwd);
 	for (;;) {
+		if (offset <= min_offset)
+			goto non_git;
 		gitfile_dir = read_gitfile_gently(DEFAULT_GIT_DIR_ENVIRONMENT);
 		if (gitfile_dir) {
 			if (set_git_dir(gitfile_dir))
@@ -460,6 +462,7 @@ const char *setup_git_directory_gently(int *nongit_ok)
 		chdir("..");
 		do {
 			if (offset <= min_offset) {
+non_git:
 				if (nongit_ok) {
 					if (chdir(cwd))
 						die("Cannot come back to cwd");
t/t1504-ceiling-directories.sh +9 -0
index 6c8757d..edc00be 100644
@@ -30,6 +30,15 @@ test_expect_success 'with non-matching ceiling directory' '
 
 '
 
+test_expect_success 'with matching ceiling directory' '
+
+	GIT_CEILING_DIRECTORIES="$CWD" &&
+	export GIT_CEILING_DIRECTORIES &&
+	(cd subdir && test_must_fail git rev-parse --git-dir) &&
+	test_must_fail git rev-parse --git-dir
+
+'
+
 test_expect_success 'with matching ceiling directories' '
 
 	GIT_CEILING_DIRECTORIES="$CWD/X:$CWD/subdir" &&

-- snap --

I will post the squashed patch as a response to this mail.

Ciao,
Dscho

#17 lore raw

Re: [SQUASHED PATCH] Add support for GIT_CEILING_DIRECTORIES

Quoted 34 lines · Johannes Schindelin

Hi,

Quoted 10 lines · David Reiss

Sorry I missed this before. As you said, in Johannes's version the ceiling directories are the last directories we look in, whereas in my implementation, they are the first directories we do not look in. I made this choice because it makes more sense for me to set my ceiling to "/home", rather than "/home/dreiss", so it will work even if I am in another user's homedir.

There is also a difference in how they handle the case where the cwd is a ceiling directory, but I think it is worth sorting out the first issue first.

Please do not top-post.

This is the interdiff to the last squashed patch:

-- snip --

setup.c +2 -0
index cece3e4..2f7a17a 100644
@@ -441,6 +441,8 @@ const char *setup_git_directory_gently(int *nongit_ok)
 	 */
 	offset = len = strlen(cwd);
 	for (;;) {
+		if (offset <= min_offset)
+			goto non_git;
 		gitfile_dir = read_gitfile_gently(DEFAULT_GIT_DIR_ENVIRONMENT);
 		if (gitfile_dir) {
 			if (set_git_dir(gitfile_dir))

setup.c const char *setup_git_directory_gently(int *nongit_ok) · line 468

Quoted 7 lines
setup.c +1 -0
@@ -460,6 +462,7 @@ const char *setup_git_directory_gently(int *nongit_ok)
 		chdir("..");
 		do {
 			if (offset <= min_offset) {
+non_git:
 				if (nongit_ok) {
 					if (chdir(cwd))
 						die("Cannot come back to cwd");

Hmm... If the implementation needs a 'goto', then I have the strong suspicion that there's already something wrong at the concept level.

I actually like the previous version better because of its clearer semantics:

- The current directory is always checked.
- GIT_CEILING_DIRECTORIES are checked. Consequently, setting the variable to the empty string is equivalent to not setting it at all.

(but it means that David can't have what he wants, i.e. he must set GIT_CEILING_DIRECTORIES=/home/dreiss.)

This implementation:

- Never checks the root directory, even if it is the current directory.
- Otherwise always checks the current directory, even if it is mentioned in GIT_CEILING_DIRECTORIES.

[That said, I'm not in strong support of this feature in general - I'm just caring because *if* it goes in, it will have conflicts with the mingw branch.]

-- Hannes

#18 lore raw

Re: [SQUASHED PATCH] Add support for GIT_CEILING_DIRECTORIES

Hi,

Quoted 18 lines · Johannes Sixt
Quoted 16 lines · Johannes Schindelin
setup.c +2 -0
index cece3e4..2f7a17a 100644
@@ -441,6 +441,8 @@ const char *setup_git_directory_gently(int *nongit_ok)
 	 */
 	offset = len = strlen(cwd);
 	for (;;) {
+		if (offset <= min_offset)
+			goto non_git;
 		gitfile_dir = read_gitfile_gently(DEFAULT_GIT_DIR_ENVIRONMENT);
 		if (gitfile_dir) {
 			if (set_git_dir(gitfile_dir))
@@ -460,6 +462,7 @@ const char *setup_git_directory_gently(int *nongit_ok)
 		chdir("..");
 		do {
Quoted 8 lines

setup.c const char *setup_git_directory_gently(int *nongit_ok) · line 468

Quoted 5 lines
setup.c +1 -0
 			if (offset <= min_offset) {
+non_git:
 				if (nongit_ok) {
 					if (chdir(cwd))
 						die("Cannot come back to cwd");

Hmm... If the implementation needs a 'goto', then I have the strong suspicion that there's already something wrong at the concept level.

I do not share the notion that "goto" = BAD.

Quoted 3 lines

[That said, I'm not in strong support of this feature in general - I'm just caring because *if* it goes in, it will have conflicts with the mingw branch.]

I am not in support of this feature at all, since I do not need it.

However, I saw that David needs it, but I did not agree with the way he implemented his patch. So I tried to show how it is possible to do it in a way that looks simpler to me.

Now the ball is back in David's field: he can just take my patch, mangle it until it does what he wants, and resubmit. That said, if the result offends my eye again, I will complain again.

Ciao,
Dscho

#19 lore raw

PATCH 1/1 Add support for GIT_CEILING_DIRECTORIES

In reply to Johannes Schindelin

In certain setups, trying to access a non-existing .git/ can take quite some time, for example when the directory is an automount directory.

Allow the user to specify directories where Git would stop looking for a .git/ directory: GIT_CEILING_DIRECTORIES, if set, is expected to be a colon delimited list of such barrier directories.

Note: if GIT_CEILING_DIRECTORIES=/a/b and your current working directory is /a, Git will _not_ stop looking.

Note2: you must not specify the directories with trailing slashes.

Initial implementation by David Reiss.

---
 Documentation/git.txt          |    6 ++++
 cache.h                        |    2 +
 path.c                         |   25 ++++++++++++++++
 setup.c                        |   14 +++++++-
 t/t1504-ceiling-directories.sh |   63 ++++++++++++++++++++++++++++++++++++++++
 t/test-lib.sh                  |    1 +
 6 files changed, 109 insertions(+), 2 deletions(-)
 create mode 100644 t/t1504-ceiling-directories.sh
Documentation/git.txt +6 -0
index adcd3e0..e4413bf 100644
@@ -415,6 +415,12 @@ 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_DIRECTORIES'::
+	If set (to a colon delimited list of absolute directories), Git
+	will refuse to look for the .git/ directory further when hitting
+	one of those directories (otherwise it would traverse the parent
+	directories until hitting the root directory).
+
 git Commits
 ~~~~~~~~~~~
 'GIT_AUTHOR_NAME'::
cache.h +2 -0
index a8638b1..c31b4c7 100644
@@ -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_DIRECTORIES_ENVIRONMENT "GIT_CEILING_DIRECTORIES"
 #define GITATTRIBUTES_FILE ".gitattributes"
 #define INFOATTRIBUTES_FILE "info/attributes"
 #define ATTRIBUTE_MACRO_PREFIX "[attr]"
@@ -522,6 +523,7 @@ static inline int is_absolute_path(const char *path)
 	return path[0] == '/';
 }
 const char *make_absolute_path(const char *path);
+int longest_prefix(const char *path, const char *prefix_list);
 
 /* Read and unpack a sha1 file into memory, write memory to a sha1 file */
 extern int sha1_object_info(const unsigned char *, unsigned long *);
path.c +25 -0
index b7c24a2..a097ecc 100644
@@ -357,3 +357,28 @@ const char *make_absolute_path(const char *path)
 
 	return buf;
 }
+
+static int is_separator(char c)
+{
+	return !c || c == '/';
+}
+
+int longest_prefix(const char *path, const char *prefix_list)
+{
+	int max_length = 0, length = 0, i;
+
+	for (i = 0; prefix_list[i]; i++)
+		if (prefix_list[i] == ':') {
+			if (length > max_length && is_separator(path[length]))
+				max_length = length;
+			length = 0;
+		}
+		else if (length >= 0) {
+			if (prefix_list[i] == path[length])
+				length++;
+			else
+				length = -1;
+		}
+	return max_length > length || !is_separator(path[length]) ?
+		max_length : length;
+}
setup.c +12 -2
index 9e9a2b1..2f7a17a 100644
@@ -365,10 +365,13 @@ const char *read_gitfile_gently(const char *path)
 const char *setup_git_directory_gently(int *nongit_ok)
 {
 	const char *work_tree_env = getenv(GIT_WORK_TREE_ENVIRONMENT);
+	const char *ceiling_directories =
+		getenv(CEILING_DIRECTORIES_ENVIRONMENT);
 	static char cwd[PATH_MAX+1];
 	const char *gitdirenv;
 	const char *gitfile_dir;
 	int len, offset;
+	int min_offset = 0;
 
 	/*
 	 * Let's assume that we are in a git repository.
@@ -422,6 +425,9 @@ const char *setup_git_directory_gently(int *nongit_ok)
 	if (!getcwd(cwd, sizeof(cwd)-1))
 		die("Unable to read current working directory");
 
+	if (ceiling_directories)
+		min_offset = longest_prefix(cwd, ceiling_directories);
+
 	/*
 	 * Test in the following order (relative to the cwd):
 	 * - .git (file containing "gitdir: <path>")
@@ -435,6 +441,8 @@ const char *setup_git_directory_gently(int *nongit_ok)
 	 */
 	offset = len = strlen(cwd);
 	for (;;) {
+		if (offset <= min_offset)
+			goto non_git;
 		gitfile_dir = read_gitfile_gently(DEFAULT_GIT_DIR_ENVIRONMENT);
 		if (gitfile_dir) {
 			if (set_git_dir(gitfile_dir))
@@ -453,7 +461,8 @@ const char *setup_git_directory_gently(int *nongit_ok)
 		}
 		chdir("..");
 		do {
-			if (!offset) {
+			if (offset <= min_offset) {
+non_git:
 				if (nongit_ok) {
 					if (chdir(cwd))
 						die("Cannot come back to cwd");
@@ -462,7 +471,8 @@ const char *setup_git_directory_gently(int *nongit_ok)
 				}
 				die("Not a git repository");
 			}
-		} while (cwd[--offset] != '/');
+		} while (offset > min_offset &&
+				--offset >=0 && cwd[offset] != '/');
 	}
 
 	inside_git_dir = 0;
t/t1504-ceiling-directories.sh new file +63 -0

63 lines not on this page · raw message

t/test-lib.sh +1 -0
index 5002fb0..c3a3167 100644
@@ -35,6 +35,7 @@ unset GIT_WORK_TREE
 unset GIT_EXTERNAL_DIFF
 unset GIT_INDEX_FILE
 unset GIT_OBJECT_DIRECTORY
+unset GIT_CEILING_DIRECTORIES
 unset SHA1_FILE_DIRECTORIES
 unset SHA1_FILE_DIRECTORY
 GIT_MERGE_VERBOSITY=5
Signature · 1 line
1.5.5.1.490.g1644c11