Provide a mechanism to turn off symlink resolution in ceiling paths

Commit 1b77d83cab 'setup_git_directory_gently_1(): resolve symlinks in ceiling paths' changed the setup code to resolve symlinks in the entries in GIT_CEILING_DIRECTORIES. Because those entries are compared textually to the symlink-resolved current directory, an entry in GIT_CEILING_DIRECTORIES that contained a symlink would have no effect. It was known that this could cause performance problems if the symlink resolution *itself* touched slow filesystems, but it was thought that such use cases would be unlikely. The intention of the earlier change was to deal with a case when the user has this: GIT_CEILING_DIRECTORIES=/home/gitster but in reality, /home/gitster is a symbolic link to somewhere else, e.g. /net/machine/home4/gitster. A textual comparison between the specified value /home/gitster and the location getcwd(3) returns would not help us, but readlink("/home/gitster") would still be fast. After this change was released, Anders Kaseorg <andersk@mit.edu> reported: > [...] my computer has been acting so slow when I’m not connected to > the network. I put various network filesystem paths in > $GIT_CEILING_DIRECTORIES, such as > /afs/athena.mit.edu/user/a/n/andersk (to avoid hitting its parents > /afs/athena.mit.edu, /afs/athena.mit.edu/user/a, and > /afs/athena.mit.edu/user/a/n which all live in different AFS > volumes). Now when I’m not connected to the network, every > invocation of Git, including the __git_ps1 in my shell prompt, waits > for AFS to timeout. To allow users to work around this problem, give them a mechanism to turn off symlink resolution in GIT_CEILING_DIRECTORIES entries. All the entries that follow an empty entry will not be checked for symbolic links and used literally in comparison. E.g. with these: GIT_CEILING_DIRECTORIES=:/foo/bar:/xyzzy or GIT_CEILING_DIRECTORIES=/foo/bar::/xyzzy we will not readlink("/xyzzy") because it comes after an empty entry. With the former (but not with the latter), "/foo/bar" comes after an empty entry, and we will not readlink it, either. Signed-off-by: Michael Haggerty <mhagger@alum.mit.edu> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Michael Haggerty committed Feb 20, 2013 at 10:09 UTC 7ec30aaa5ba2a71287879d7bd9a5c55363ea1bbe
3 files changed +52 -16
Documentation/git.txt
+13 -6
@@ -653,12 +653,19 @@ git so take care if using Cogito etc.
653 The '--namespace' command-line option also sets this value.
654
655 'GIT_CEILING_DIRECTORIES'::
656 - This should be a colon-separated list of absolute paths.
657 - If set, it is a list of directories that git should not chdir
658 - up into while looking for a repository directory.
659 - It will not exclude the current working directory or
660 - a GIT_DIR set on the command line or in the environment.
661 - (Useful for excluding slow-loading network directories.)
656 + This should be a colon-separated list of absolute paths. If
657 + set, it is a list of directories that git should not chdir up
658 + into while looking for a repository directory (useful for
659 + excluding slow-loading network directories). It will not
660 + exclude the current working directory or a GIT_DIR set on the
661 + command line or in the environment. Normally, Git has to read
662 + the entries in this list and resolve any symlink that
663 + might be present in order to compare them with the current
664 + directory. However, if even this access is slow, you
665 + can add an empty entry to the list to tell Git that the
666 + subsequent entries are not symlinks and needn't be resolved;
667 + e.g.,
668 + 'GIT_CEILING_DIRECTORIES=/maybe/symlink::/very/slow/non/symlink'.
669
670 'GIT_DISCOVERY_ACROSS_FILESYSTEM'::
671 When run in a directory that does not have ".git" repository
setup.c
+22 -10
@@ -624,22 +624,32 @@ static dev_t get_device_or_die(const char *path, const char *prefix, int prefix_
624 /*
625 * A "string_list_each_func_t" function that canonicalizes an entry
626 * from GIT_CEILING_DIRECTORIES using real_path_if_valid(), or
627 - * discards it if unusable.
627 + * discards it if unusable. The presence of an empty entry in
628 + * GIT_CEILING_DIRECTORIES turns off canonicalization for all
629 + * subsequent entries.
630 */
631 static int canonicalize_ceiling_entry(struct string_list_item *item,
630 - void *unused)
632 + void *cb_data)
633 {
634 + int *empty_entry_found = cb_data;
635 char *ceil = item->string;
633 - const char *real_path;
636
635 - if (!*ceil || !is_absolute_path(ceil))
637 + if (!*ceil) {
638 + *empty_entry_found = 1;
639 return 0;
637 - real_path = real_path_if_valid(ceil);
638 - if (!real_path)
640 + } else if (!is_absolute_path(ceil)) {
641 return 0;
640 - free(item->string);
641 - item->string = xstrdup(real_path);
642 - return 1;
642 + } else if (*empty_entry_found) {
643 + /* Keep entry but do not canonicalize it */
644 + return 1;
645 + } else {
646 + const char *real_path = real_path_if_valid(ceil);
647 + if (!real_path)
648 + return 0;
649 + free(item->string);
650 + item->string = xstrdup(real_path);
651 + return 1;
652 + }
653 }
654
655 /*
@@ -679,9 +689,11 @@ static const char *setup_git_directory_gently_1(int *nongit_ok)
689 return setup_explicit_git_dir(gitdirenv, cwd, len, nongit_ok);
690
691 if (env_ceiling_dirs) {
692 + int empty_entry_found = 0;
693 +
694 string_list_split(&ceiling_dirs, env_ceiling_dirs, PATH_SEP, -1);
695 filter_string_list(&ceiling_dirs, 0,
684 - canonicalize_ceiling_entry, NULL);
696 + canonicalize_ceiling_entry, &empty_entry_found);
697 ceil_offset = longest_ancestor_length(cwd, &ceiling_dirs);
698 string_list_clear(&ceiling_dirs, 0);
699 }
t/t1504-ceiling-dirs.sh
+17
@@ -44,6 +44,10 @@ test_prefix ceil_at_sub ""
44 GIT_CEILING_DIRECTORIES="$TRASH_ROOT/sub/"
45 test_prefix ceil_at_sub_slash ""
46
47 +if test_have_prereq SYMLINKS
48 +then
49 + ln -s sub top
50 +fi
51
52 mkdir -p sub/dir || exit 1
53 cd sub/dir || exit 1
@@ -68,6 +72,19 @@ test_fail subdir_ceil_at_sub
72 GIT_CEILING_DIRECTORIES="$TRASH_ROOT/sub/"
73 test_fail subdir_ceil_at_sub_slash
74
75 +if test_have_prereq SYMLINKS
76 +then
77 + GIT_CEILING_DIRECTORIES="$TRASH_ROOT/top"
78 + test_fail subdir_ceil_at_top
79 + GIT_CEILING_DIRECTORIES="$TRASH_ROOT/top/"
80 + test_fail subdir_ceil_at_top_slash
81 +
82 + GIT_CEILING_DIRECTORIES=":$TRASH_ROOT/top"
83 + test_prefix subdir_ceil_at_top_no_resolve "sub/dir/"
84 + GIT_CEILING_DIRECTORIES=":$TRASH_ROOT/top/"
85 + test_prefix subdir_ceil_at_top_slash_no_resolve "sub/dir/"
86 +fi
87 +
88 GIT_CEILING_DIRECTORIES="$TRASH_ROOT/sub/dir"
89 test_prefix subdir_ceil_at_subdir "sub/dir/"
90