{"thread":{"id":"13585","subject":"[PATCH 3/4] Add support for GIT_CEILING_DIRECTORIES","startedAt":"2008-05-20T06:49:26Z","lastAt":"2008-05-20T06:49:26Z","messageCount":1,"participants":["David Reiss"],"isPatch":true,"patchVersion":1,"patchTotal":4},"messages":[{"id":"77310","messageId":"48327476.5040500@facebook.com","threadId":"13585","inReplyTo":null,"subject":"[PATCH 3/4] Add support for GIT_CEILING_DIRECTORIES","fromName":"David Reiss","fromEmail":"dreiss@facebook.com","sentAt":"2008-05-20T06:49:26Z","receivedAt":"2008-05-20T06:49:26Z","isPatch":true,"sender":{"key":"dreiss@facebook.com","avatar":null},"body":"Make git recognize a new environment variable that prevents it from\nchdir'ing up into specified directories when looking for a GIT_DIR.\nUseful for avoiding slow network directories.\n\nFor example, I use git in an environment where homedirs are automounted\nand \"ls /home/nonexistent\" takes about 9 seconds.  Setting\nGIT_CEILING_DIRS=\"/home\" allows \"git help -a\" (for bash completion) and\n\"git symbolic-ref\" (for my shell prompt) to run in a reasonable time.\n\nSigned-off-by: David Reiss <dreiss@facebook.com>\n---\nChanged the name to GIT_CEILING_DIRECTORIES, according to Johannes's version.\n\nMoved the \"longest ancestor\" function to path.c, for the same reason.\n\nAdded path normalization to longest_ancestor_length, per Junio's request.\n\nMoved the tests out of setup.c and into a normal shell script test.\n\nAdded tests for a few edge cases per Junio's request.  I only added them to\nthe test of longest_ancestor_length rather than the full functional test,\nbecause the length is the only interesting input to the setup function.\n\nTried to simplify things as much as possible.  The diff --stat for setup.c\nis 10 lines with git diff -b.\n\nThis version uses the original semantics.  Git refuses to look for .git or\nobject in any of the GIT_CEILING_DIRECTORIES when cd'ing up, but will always\ncheck the current directory, even if it is a ceiling dir.  It remains possible\nfor the root directory to be a git repo.  I think this is a good compromise\nbetween usability and preventing users who forget about ceiling dirs from\nbeing surprised when they can \"ls .git\" but Git cannot find the repo.  If\nothers would prefer different semantics, I'm happy to change them.\n\nI'm happy to help resolve conflicts with the msys branch, if someone wants\nto point me in the right direction.  Or, if it is going to be merged soon,\nI can just wait and then rebase my changes on top of it.\n\nAnd if the test suite takes too long, I'm fine commenting most of it out.\n\n Documentation/git.txt   |    8 ++\n cache.h                 |    2 +\n path.c                  |   43 ++++++++++++\n setup.c                 |   24 ++++---\n t/t0060-path-utils.sh   |   47 ++++++++++++++\n t/t1504-ceiling-dirs.sh |  163 +++++++++++++++++++++++++++++++++++++++++++++++\n t/test-lib.sh           |    1 +\n test-path-utils.c       |    5 ++\n 8 files changed, 282 insertions(+), 11 deletions(-)\n create mode 100755 t/t1504-ceiling-dirs.sh\n\ndiff --git a/Documentation/git.txt b/Documentation/git.txt\nindex 6f445b1..0c624fc 100644\n--- a/Documentation/git.txt\n+++ b/Documentation/git.txt\n@@ -415,6 +415,14 @@ git so take care if using Cogito etc.\n \tThis can also be controlled by the '--work-tree' command line\n \toption and the core.worktree configuration variable.\n \n+'GIT_CEILING_DIRECTORIES'::\n+\tThis should be a colon-separated list of absolute paths.\n+\tIf set, it is a list of directories that git should not chdir\n+\tup into while looking for a repository directory.\n+\tIt will not exclude the current working directory or\n+\ta GIT_DIR set on the command line or in the environment.\n+\t(Useful for excluding slow-loading network directories.)\n+\n git Commits\n ~~~~~~~~~~~\n 'GIT_AUTHOR_NAME'::\ndiff --git a/cache.h b/cache.h\nindex 5fd1e5b..c739a86 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -300,6 +300,7 @@ static inline enum object_type object_type(unsigned int mode)\n #define CONFIG_ENVIRONMENT \"GIT_CONFIG\"\n #define CONFIG_LOCAL_ENVIRONMENT \"GIT_CONFIG_LOCAL\"\n #define EXEC_PATH_ENVIRONMENT \"GIT_EXEC_PATH\"\n+#define CEILING_DIRECTORIES_ENVIRONMENT \"GIT_CEILING_DIRECTORIES\"\n #define GITATTRIBUTES_FILE \".gitattributes\"\n #define INFOATTRIBUTES_FILE \"info/attributes\"\n #define ATTRIBUTE_MACRO_PREFIX \"[attr]\"\n@@ -515,6 +516,7 @@ static inline int is_absolute_path(const char *path)\n }\n const char *make_absolute_path(const char *path);\n int normalize_absolute_path(char *buf, const char *path);\n+int longest_ancestor_length(const char *path, const char *prefix_list);\n \n /* Read and unpack a sha1 file into memory, write memory to a sha1 file */\n extern int sha1_object_info(const unsigned char *, unsigned long *);\ndiff --git a/path.c b/path.c\nindex 17921ac..4176cfb 100644\n--- a/path.c\n+++ b/path.c\n@@ -407,3 +407,46 @@ int normalize_absolute_path(char *buf, const char *path)\n \t*dst = '\\0';\n \treturn dst - buf;\n }\n+\n+/*\n+ * path = Canonical absolute path\n+ * prefix_list = Colon-separated list of absolute paths\n+ *\n+ * Determines, for each path in parent_list, whether the \"prefix\" really\n+ * is an ancestor directory of path.  Returns the length of the longest\n+ * ancestor directory, excluding any trailing slashes, or -1 if no prefix\n+ * is an ancestor.  (Note that this means 0 is returned if prefix_list is\n+ * \"/\".) \"/foo\" is not considered an ancestor of \"/foobar\".  Directories\n+ * are not considered to be their own ancestors.  path must be in a\n+ * canonical form: empty components, or \".\" or \"..\" components are not\n+ * allowed.  prefix_list may be null, which is like \"\".\n+ */\n+int longest_ancestor_length(const char *path, const char *prefix_list)\n+{\n+\tchar buf[PATH_MAX+1];\n+\tconst char *ceil, *colon;\n+\tint len, max_len = -1;\n+\n+\tif (prefix_list == NULL || !strcmp(path, \"/\"))\n+\t\treturn -1;\n+\n+\tfor (colon = ceil = prefix_list; *colon; ceil = colon+1) {\n+\t\tfor (colon = ceil; *colon && *colon != ':'; colon++);\n+\t\tlen = colon - ceil;\n+\t\tif (len == 0 || len > PATH_MAX || !is_absolute_path(ceil))\n+\t\t\tcontinue;\n+\t\tstrlcpy(buf, ceil, len+1);\n+\t\tlen = normalize_absolute_path(buf, buf);\n+\t\t/* Strip \"trailing slashes\" from \"/\". */\n+\t\tif (len == 1)\n+\t\t\tlen = 0;\n+\n+\t\tif (!strncmp(path, buf, len) &&\n+\t\t    path[len] == '/' &&\n+\t\t    len > max_len) {\n+\t\t\tmax_len = len;\n+\t\t}\n+\t}\n+\n+\treturn max_len;\n+}\ndiff --git a/setup.c b/setup.c\nindex b8fd476..c14b106 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -359,10 +359,11 @@ const char *read_gitfile_gently(const char *path)\n const char *setup_git_directory_gently(int *nongit_ok)\n {\n \tconst char *work_tree_env = getenv(GIT_WORK_TREE_ENVIRONMENT);\n+\tconst char *env_ceiling_dirs = getenv(CEILING_DIRECTORIES_ENVIRONMENT);\n \tstatic char cwd[PATH_MAX+1];\n \tconst char *gitdirenv;\n \tconst char *gitfile_dir;\n-\tint len, offset;\n+\tint len, offset, ceil_offset;\n \n \t/*\n \t * Let's assume that we are in a git repository.\n@@ -414,6 +415,8 @@ const char *setup_git_directory_gently(int *nongit_ok)\n \tif (!getcwd(cwd, sizeof(cwd)-1))\n \t\tdie(\"Unable to read current working directory\");\n \n+\tceil_offset = longest_ancestor_length(cwd, env_ceiling_dirs);\n+\n \t/*\n \t * Test in the following order (relative to the cwd):\n \t * - .git (file containing \"gitdir: <path>\")\n@@ -444,17 +447,16 @@ const char *setup_git_directory_gently(int *nongit_ok)\n \t\t\treturn NULL;\n \t\t}\n \t\tchdir(\"..\");\n-\t\tdo {\n-\t\t\tif (!offset) {\n-\t\t\t\tif (nongit_ok) {\n-\t\t\t\t\tif (chdir(cwd))\n-\t\t\t\t\t\tdie(\"Cannot come back to cwd\");\n-\t\t\t\t\t*nongit_ok = 1;\n-\t\t\t\t\treturn NULL;\n-\t\t\t\t}\n-\t\t\t\tdie(\"Not a git repository\");\n+\t\twhile (--offset > ceil_offset && cwd[offset] != '/');\n+\t\tif (offset <= ceil_offset) {\n+\t\t\tif (nongit_ok) {\n+\t\t\t\tif (chdir(cwd))\n+\t\t\t\t\tdie(\"Cannot come back to cwd\");\n+\t\t\t\t*nongit_ok = 1;\n+\t\t\t\treturn NULL;\n \t\t\t}\n-\t\t} while (cwd[--offset] != '/');\n+\t\t\tdie(\"Not a git repository\");\n+\t\t}\n \t}\n \n \tinside_git_dir = 0;\ndiff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh\nindex 9076b3b..6e7501f 100755\n--- a/t/t0060-path-utils.sh\n+++ b/t/t0060-path-utils.sh\n@@ -12,6 +12,11 @@ norm_abs() {\n \t\"test \\$(test-path-utils normalize_absolute_path '$1') = '$2'\"\n }\n \n+ancestor() {\n+\ttest_expect_success \"longest ancestor\" \\\n+\t\"test \\$(test-path-utils longest_ancestor_length '$1' '$2') = '$3'\"\n+}\n+\n norm_abs \"\" /\n norm_abs / /\n norm_abs // /\n@@ -37,4 +42,46 @@ norm_abs /d1/s1//../s2/../../d2 /d2\n norm_abs /d1/.../d2 /d1/.../d2\n norm_abs /d1/..././../d2 /d1/d2\n \n+ancestor / \"\" -1\n+ancestor / / -1\n+ancestor /foo \"\" -1\n+ancestor /foo : -1\n+ancestor /foo ::. -1\n+ancestor /foo ::..:: -1\n+ancestor /foo / 0\n+ancestor /foo /fo -1\n+ancestor /foo /foo -1\n+ancestor /foo /foo/ -1\n+ancestor /foo /bar -1\n+ancestor /foo /bar/ -1\n+ancestor /foo /foo/bar -1\n+ancestor /foo /foo:/bar/ -1\n+ancestor /foo /foo/:/bar/ -1\n+ancestor /foo /foo::/bar/ -1\n+ancestor /foo /:/foo:/bar/ 0\n+ancestor /foo /foo:/:/bar/ 0\n+ancestor /foo /:/bar/:/foo 0\n+ancestor /foo/bar \"\" -1\n+ancestor /foo/bar / 0\n+ancestor /foo/bar /fo -1\n+ancestor /foo/bar foo -1\n+ancestor /foo/bar /foo 4\n+ancestor /foo/bar /foo/ 4\n+ancestor /foo/bar /foo/ba -1\n+ancestor /foo/bar /:/fo 0\n+ancestor /foo/bar /foo:/foo/ba 4\n+ancestor /foo/bar /bar -1\n+ancestor /foo/bar /bar/ -1\n+ancestor /foo/bar /fo: -1\n+ancestor /foo/bar :/fo -1\n+ancestor /foo/bar /foo:/bar/ 4\n+ancestor /foo/bar /:/foo:/bar/ 4\n+ancestor /foo/bar /foo:/:/bar/ 4\n+ancestor /foo/bar /:/bar/:/fo 0\n+ancestor /foo/bar /:/bar/ 0\n+ancestor /foo/bar :://foo/. 4\n+ancestor /foo/bar :://foo/.:: 4\n+ancestor /foo/bar //foo/./::/bar 4\n+ancestor /foo/bar ::/bar -1\n+\n test_done\ndiff --git a/t/t1504-ceiling-dirs.sh b/t/t1504-ceiling-dirs.sh\nnew file mode 100755\nindex 0000000..91b704a\n--- /dev/null\n+++ b/t/t1504-ceiling-dirs.sh\n@@ -0,0 +1,163 @@\n+#!/bin/sh\n+\n+test_description='test GIT_CEILING_DIRECTORIES'\n+. ./test-lib.sh\n+\n+test_prefix() {\n+\ttest_expect_success \"$1\" \\\n+\t\"test '$2' = \\\"\\$(git rev-parse --show-prefix)\\\"\"\n+}\n+\n+test_fail() {\n+\ttest_expect_code 128 \"$1: prefix\" \\\n+\t\"git rev-parse --show-prefix\"\n+}\n+\n+TRASH_ROOT=\"$(pwd)\"\n+ROOT_PARENT=$(dirname \"$TRASH_ROOT\")\n+\n+\n+unset GIT_CEILING_DIRECTORIES\n+test_prefix no_ceil \"\"\n+\n+export GIT_CEILING_DIRECTORIES\n+\n+GIT_CEILING_DIRECTORIES=\"\"\n+test_prefix ceil_empty \"\"\n+\n+GIT_CEILING_DIRECTORIES=\"$ROOT_PARENT\"\n+test_prefix ceil_at_parent \"\"\n+\n+GIT_CEILING_DIRECTORIES=\"$ROOT_PARENT/\"\n+test_prefix ceil_at_parent_slash \"\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT\"\n+test_prefix ceil_at_trash \"\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/\"\n+test_prefix ceil_at_trash_slash \"\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/sub\"\n+test_prefix ceil_at_sub \"\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/sub/\"\n+test_prefix ceil_at_sub_slash \"\"\n+\n+\n+mkdir -p sub/dir || exit 1\n+cd sub/dir || exit 1\n+\n+unset GIT_CEILING_DIRECTORIES\n+test_prefix subdir_no_ceil \"sub/dir/\"\n+\n+export GIT_CEILING_DIRECTORIES\n+\n+GIT_CEILING_DIRECTORIES=\"\"\n+test_prefix subdir_ceil_empty \"sub/dir/\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT\"\n+test_fail subdir_ceil_at_trash\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/\"\n+test_fail subdir_ceil_at_trash_slash\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/sub\"\n+test_fail subdir_ceil_at_sub\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/sub/\"\n+test_fail subdir_ceil_at_sub_slash\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/sub/dir\"\n+test_prefix subdir_ceil_at_subdir \"sub/dir/\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/sub/dir/\"\n+test_prefix subdir_ceil_at_subdir_slash \"sub/dir/\"\n+\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/su\"\n+test_prefix subdir_ceil_at_su \"sub/dir/\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/su/\"\n+test_prefix subdir_ceil_at_su_slash \"sub/dir/\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/sub/di\"\n+test_prefix subdir_ceil_at_sub_di \"sub/dir/\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/sub/di\"\n+test_prefix subdir_ceil_at_sub_di_slash \"sub/dir/\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/subdi\"\n+test_prefix subdir_ceil_at_subdi \"sub/dir/\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/subdi\"\n+test_prefix subdir_ceil_at_subdi_slash \"sub/dir/\"\n+\n+\n+GIT_CEILING_DIRECTORIES=\"foo:$TRASH_ROOT/sub\"\n+test_fail second_of_two\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/sub:bar\"\n+test_fail first_of_two\n+\n+GIT_CEILING_DIRECTORIES=\"foo:$TRASH_ROOT/sub:bar\"\n+test_fail second_of_three\n+\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/sub\"\n+GIT_DIR=../../.git\n+export GIT_DIR\n+test_prefix git_dir_specified \"\"\n+unset GIT_DIR\n+\n+\n+cd ../.. || exit 1\n+mkdir -p s/d || exit 1\n+cd s/d || exit 1\n+\n+unset GIT_CEILING_DIRECTORIES\n+test_prefix sd_no_ceil \"s/d/\"\n+\n+export GIT_CEILING_DIRECTORIES\n+\n+GIT_CEILING_DIRECTORIES=\"\"\n+test_prefix sd_ceil_empty \"s/d/\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT\"\n+test_fail sd_ceil_at_trash\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/\"\n+test_fail sd_ceil_at_trash_slash\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/s\"\n+test_fail sd_ceil_at_s\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/s/\"\n+test_fail sd_ceil_at_s_slash\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/s/d\"\n+test_prefix sd_ceil_at_sd \"s/d/\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/s/d/\"\n+test_prefix sd_ceil_at_sd_slash \"s/d/\"\n+\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/su\"\n+test_prefix sd_ceil_at_su \"s/d/\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/su/\"\n+test_prefix sd_ceil_at_su_slash \"s/d/\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/s/di\"\n+test_prefix sd_ceil_at_s_di \"s/d/\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/s/di\"\n+test_prefix sd_ceil_at_s_di_slash \"s/d/\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/sdi\"\n+test_prefix sd_ceil_at_sdi \"s/d/\"\n+\n+GIT_CEILING_DIRECTORIES=\"$TRASH_ROOT/sdi\"\n+test_prefix sd_ceil_at_sdi_slash \"s/d/\"\n+\n+\n+test_done\ndiff --git a/t/test-lib.sh b/t/test-lib.sh\nindex 7c2a8ba..302c4bd 100644\n--- a/t/test-lib.sh\n+++ b/t/test-lib.sh\n@@ -35,6 +35,7 @@ unset GIT_WORK_TREE\n unset GIT_EXTERNAL_DIFF\n unset GIT_INDEX_FILE\n unset GIT_OBJECT_DIRECTORY\n+unset GIT_CEILING_DIRECTORIES\n unset SHA1_FILE_DIRECTORIES\n unset SHA1_FILE_DIRECTORY\n GIT_MERGE_VERBOSITY=5\ndiff --git a/test-path-utils.c b/test-path-utils.c\nindex 842b580..a0bcb0e 100644\n--- a/test-path-utils.c\n+++ b/test-path-utils.c\n@@ -17,5 +17,10 @@ int main(int argc, char **argv)\n \t\t}\n \t}\n \n+\tif (argc == 4 && !strcmp(argv[1], \"longest_ancestor_length\")) {\n+\t\tint len = longest_ancestor_length(argv[2], argv[3]);\n+\t\tprintf(\"%d\\n\", len);\n+\t}\n+\n \treturn 0;\n }\n-- \n1.5.4\n"}]}