]> git.ipfire.org Git - thirdparty/git.git/commitdiff
setup: improve error diagnosis for invalid .git files
authorTian Yuchen <a3205153416@gmail.com>
Wed, 4 Mar 2026 14:15:26 +0000 (22:15 +0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 4 Mar 2026 17:23:48 +0000 (09:23 -0800)
'read_gitfile_gently()' treats any non-regular file as
'READ_GITFILE_ERR_NOT_A_FILE' and fails to discern between 'ENOENT'
and other stat failures. This flawed error reporting is noted by two
'NEEDSWORK' comments.

Address these comments by introducing two new error codes:
'READ_GITFILE_ERR_MISSING'(which groups the "file missing" scenarios
together) and 'READ_GITFILE_ERR_IS_A_DIR':

1. Update 'read_gitfile_error_die()' to treat 'IS_A_DIR', 'MISSING',
'NOT_A_FILE' and 'STAT_FAILED' as non-fatal no-ops. This accommodates
intentional non-repo scenarios (e.g., GIT_DIR=/dev/null).

2. Explicitly catch 'NOT_A_FILE' and 'STAT_FAILED' during
discovery and call 'die()' if 'die_on_error' is set.

3. Unconditionally pass '&error_code' to 'read_gitfile_gently()'.

4. Only invoke 'is_git_directory()' when we explicitly receive
   'READ_GITFILE_ERR_IS_A_DIR', avoiding redundant checks.

Additionally, audit external callers of 'read_gitfile_gently()' in
'submodule.c' and 'worktree.c' to accommodate the refined error codes.

Signed-off-by: Tian Yuchen <a3205153416@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
setup.c
setup.h
submodule.c
t/meson.build
t/t0009-git-dir-validation.sh [new file with mode: 0755]
worktree.c

diff --git a/setup.c b/setup.c
index b723f8b33931bd22246d4120afed3b7bef59c8ae..0ae9c8846612f4f0b8539c601abb28928cbd7925 100644 (file)
--- a/setup.c
+++ b/setup.c
@@ -895,8 +895,10 @@ int verify_repository_format(const struct repository_format *format,
 void read_gitfile_error_die(int error_code, const char *path, const char *dir)
 {
        switch (error_code) {
-       case READ_GITFILE_ERR_STAT_FAILED:
        case READ_GITFILE_ERR_NOT_A_FILE:
+       case READ_GITFILE_ERR_STAT_FAILED:
+       case READ_GITFILE_ERR_MISSING:
+       case READ_GITFILE_ERR_IS_A_DIR:
                /* non-fatal; follow return path */
                break;
        case READ_GITFILE_ERR_OPEN_FAILED:
@@ -939,8 +941,14 @@ const char *read_gitfile_gently(const char *path, int *return_error_code)
        static struct strbuf realpath = STRBUF_INIT;
 
        if (stat(path, &st)) {
-               /* NEEDSWORK: discern between ENOENT vs other errors */
-               error_code = READ_GITFILE_ERR_STAT_FAILED;
+               if (errno == ENOENT || errno == ENOTDIR)
+                       error_code = READ_GITFILE_ERR_MISSING;
+               else
+                       error_code = READ_GITFILE_ERR_STAT_FAILED;
+               goto cleanup_return;
+       }
+       if (S_ISDIR(st.st_mode)) {
+               error_code = READ_GITFILE_ERR_IS_A_DIR;
                goto cleanup_return;
        }
        if (!S_ISREG(st.st_mode)) {
@@ -1576,20 +1584,37 @@ static enum discovery_result setup_git_directory_gently_1(struct strbuf *dir,
                if (offset > min_offset)
                        strbuf_addch(dir, '/');
                strbuf_addstr(dir, DEFAULT_GIT_DIR_ENVIRONMENT);
-               gitdirenv = read_gitfile_gently(dir->buf, die_on_error ?
-                                               NULL : &error_code);
+               gitdirenv = read_gitfile_gently(dir->buf, &error_code);
                if (!gitdirenv) {
-                       if (die_on_error ||
-                           error_code == READ_GITFILE_ERR_NOT_A_FILE) {
-                               /* NEEDSWORK: fail if .git is not file nor dir */
+                       switch (error_code) {
+                       case READ_GITFILE_ERR_MISSING:
+                               /* no .git in this directory, move on */
+                               break;
+                       case READ_GITFILE_ERR_IS_A_DIR:
                                if (is_git_directory(dir->buf)) {
                                        gitdirenv = DEFAULT_GIT_DIR_ENVIRONMENT;
                                        gitdir_path = xstrdup(dir->buf);
                                }
-                       } else if (error_code != READ_GITFILE_ERR_STAT_FAILED)
-                               return GIT_DIR_INVALID_GITFILE;
-               } else
+                               break;
+                       case READ_GITFILE_ERR_STAT_FAILED:
+                               if (die_on_error)
+                                       die(_("error reading '%s'"), dir->buf);
+                               else
+                                       return GIT_DIR_INVALID_GITFILE;
+                       case READ_GITFILE_ERR_NOT_A_FILE:
+                               if (die_on_error)
+                                       die(_("not a regular file: '%s'"), dir->buf);
+                               else
+                                       return GIT_DIR_INVALID_GITFILE;
+                       default:
+                               if (die_on_error)
+                                       read_gitfile_error_die(error_code, dir->buf, NULL);
+                               else
+                                       return GIT_DIR_INVALID_GITFILE;
+                       }
+               } else {
                        gitfile = xstrdup(dir->buf);
+               }
                /*
                 * Earlier, we tentatively added DEFAULT_GIT_DIR_ENVIRONMENT
                 * to check that directory for a repository.
diff --git a/setup.h b/setup.h
index d55dcc66086308b31d86f28bcbb84f5d01e4453f..1a0d010b5d2abf6f19f15467f5df3d361e53800e 100644 (file)
--- a/setup.h
+++ b/setup.h
@@ -36,6 +36,8 @@ int is_nonbare_repository_dir(struct strbuf *path);
 #define READ_GITFILE_ERR_NO_PATH 6
 #define READ_GITFILE_ERR_NOT_A_REPO 7
 #define READ_GITFILE_ERR_TOO_LARGE 8
+#define READ_GITFILE_ERR_MISSING 9
+#define READ_GITFILE_ERR_IS_A_DIR 10
 void read_gitfile_error_die(int error_code, const char *path, const char *dir);
 const char *read_gitfile_gently(const char *path, int *return_error_code);
 #define read_gitfile(path) read_gitfile_gently((path), NULL)
index 40a5c6fb9d154574ee61ed15d3f2e02642e42b03..e733f8a669e19add201203ce3b4327621e64c5cc 100644 (file)
@@ -2413,7 +2413,7 @@ void absorb_git_dir_into_superproject(const char *path,
                const struct submodule *sub;
                struct strbuf sub_gitdir = STRBUF_INIT;
 
-               if (err_code == READ_GITFILE_ERR_STAT_FAILED) {
+               if (err_code == READ_GITFILE_ERR_MISSING) {
                        /* unpopulated as expected */
                        strbuf_release(&gitdir);
                        return;
index 459c52a48972e441e9325616940f5bca13b9ec83..71a6f07a11efd112e2b3c528cfb966b636d05279 100644 (file)
@@ -80,6 +80,7 @@ integration_tests = [
   't0006-date.sh',
   't0007-git-var.sh',
   't0008-ignores.sh',
+  't0009-git-dir-validation.sh',
   't0010-racy-git.sh',
   't0012-help.sh',
   't0013-sha1dc.sh',
diff --git a/t/t0009-git-dir-validation.sh b/t/t0009-git-dir-validation.sh
new file mode 100755 (executable)
index 0000000..33d21ed
--- /dev/null
@@ -0,0 +1,77 @@
+#!/bin/sh
+
+test_description='setup: validation of .git file/directory types
+
+Verify that setup_git_directory() correctly handles:
+1. Valid .git directories (including symlinks to them).
+2. Invalid .git files (FIFOs, sockets) by erroring out.
+3. Invalid .git files (garbage) by erroring out.
+'
+
+. ./test-lib.sh
+
+test_expect_success 'setup: create parent git repository' '
+       git init parent &&
+       test_commit -C parent "root-commit"
+'
+
+test_expect_success SYMLINKS 'setup: .git as a symlink to a directory is valid' '
+       test_when_finished "rm -rf parent/link-to-dir" &&
+       mkdir -p parent/link-to-dir &&
+       (
+               cd parent/link-to-dir &&
+               git init real-repo &&
+               ln -s real-repo/.git .git &&
+               git rev-parse --git-dir >actual &&
+               echo .git >expect &&
+               test_cmp expect actual
+       )
+'
+
+test_expect_success PIPE 'setup: .git as a FIFO (named pipe) is rejected' '
+       test_when_finished "rm -rf parent/fifo-trap" &&
+       mkdir -p parent/fifo-trap &&
+       (
+               cd parent/fifo-trap &&
+               mkfifo .git &&
+               test_must_fail git rev-parse --git-dir 2>stderr &&
+               grep "not a regular file" stderr
+       )
+'
+
+test_expect_success SYMLINKS,PIPE 'setup: .git as a symlink to a FIFO is rejected' '
+       test_when_finished "rm -rf parent/symlink-fifo-trap" &&
+       mkdir -p parent/symlink-fifo-trap &&
+       (
+               cd parent/symlink-fifo-trap &&
+               mkfifo target-fifo &&
+               ln -s target-fifo .git &&
+               test_must_fail git rev-parse --git-dir 2>stderr &&
+               grep "not a regular file" stderr
+       )
+'
+
+test_expect_success 'setup: .git with garbage content is rejected' '
+       test_when_finished "rm -rf parent/garbage-trap" &&
+       mkdir -p parent/garbage-trap &&
+       (
+               cd parent/garbage-trap &&
+               echo "garbage" >.git &&
+               test_must_fail git rev-parse --git-dir 2>stderr &&
+               grep "invalid gitfile format" stderr
+       )
+'
+
+test_expect_success 'setup: .git as an empty directory is ignored' '
+       test_when_finished "rm -rf parent/empty-dir" &&
+       mkdir -p parent/empty-dir &&
+       (
+               cd parent/empty-dir &&
+               git rev-parse --git-dir >expect &&
+               mkdir .git &&
+               git rev-parse --git-dir >actual &&
+               test_cmp expect actual
+       )
+'
+
+test_done
index 9308389cb6f0296b1cc21c33b95427211f840649..d1165e1d1ce1ca96f90f006c5775f217f3e96b27 100644 (file)
@@ -653,7 +653,8 @@ static void repair_gitfile(struct worktree *wt,
                }
        }
 
-       if (err == READ_GITFILE_ERR_NOT_A_FILE)
+       if (err == READ_GITFILE_ERR_NOT_A_FILE ||
+               err == READ_GITFILE_ERR_IS_A_DIR)
                fn(1, wt->path, _(".git is not a file"), cb_data);
        else if (err)
                repair = _(".git file broken");
@@ -833,7 +834,8 @@ void repair_worktree_at_path(const char *path,
                        strbuf_addstr(&backlink, dotgit_contents);
                        strbuf_realpath_forgiving(&backlink, backlink.buf, 0);
                }
-       } else if (err == READ_GITFILE_ERR_NOT_A_FILE) {
+       } else if (err == READ_GITFILE_ERR_NOT_A_FILE ||
+                       err == READ_GITFILE_ERR_IS_A_DIR) {
                fn(1, dotgit.buf, _("unable to locate repository; .git is not a file"), cb_data);
                goto done;
        } else if (err == READ_GITFILE_ERR_NOT_A_REPO) {