]> git.ipfire.org Git - thirdparty/git.git/commitdiff
Sync with 2.41.1
authorJohannes Schindelin <johannes.schindelin@gmx.de>
Wed, 17 Apr 2024 09:39:09 +0000 (11:39 +0200)
committerJohannes Schindelin <johannes.schindelin@gmx.de>
Fri, 19 Apr 2024 10:38:46 +0000 (12:38 +0200)
* maint-2.41: (38 commits)
  Git 2.41.1
  Git 2.40.2
  Git 2.39.4
  fsck: warn about symlink pointing inside a gitdir
  core.hooksPath: add some protection while cloning
  init.templateDir: consider this config setting protected
  clone: prevent hooks from running during a clone
  Add a helper function to compare file contents
  init: refactor the template directory discovery into its own function
  find_hook(): refactor the `STRIP_EXTENSION` logic
  clone: when symbolic links collide with directories, keep the latter
  entry: report more colliding paths
  t5510: verify that D/F confusion cannot lead to an RCE
  submodule: require the submodule path to contain directories only
  clone_submodule: avoid using `access()` on directories
  submodules: submodule paths must not contain symlinks
  clone: prevent clashing git dirs when cloning submodule in parallel
  t7423: add tests for symlinked submodule directories
  has_dir_name(): do not get confused by characters < '/'
  docs: document security issues around untrusted .git dirs
  ...

30 files changed:
1  2 
.github/workflows/main.yml
Documentation/git.txt
builtin/clone.c
builtin/submodule--helper.c
builtin/upload-pack.c
ci/lib.sh
config.c
copy.c
dir.c
dir.h
entry.c
fsck.c
fsck.h
hook.c
http.c
path.c
promisor-remote.c
read-cache.c
remote-curl.c
repository.c
setup.c
setup.h
submodule.c
t/helper/test-path-utils.c
t/t0000-basic.sh
t/t1450-fsck.sh
t/t1800-hook.sh
t/t5510-fetch.sh
t/t7400-submodule-basic.sh
t/t7406-submodule-update.sh

Simple merge
Simple merge
diff --cc builtin/clone.c
index c6357af949895a688639c83984598931906b2690,d6545d036f8301ef034b78bf2776c4595d8bcf15..72e89c309a01f39e31af21e22da114540965bb8b
@@@ -930,7 -959,8 +959,9 @@@ int cmd_clone(int argc, const char **ar
        int submodule_progress;
        int filter_submodules = 0;
        int hash_algo;
 +      const int do_not_override_repo_unix_permissions = -1;
+       const char *template_dir;
+       char *template_dir_dup = NULL;
  
        struct transport_ls_refs_options transport_ls_refs_options =
                TRANSPORT_LS_REFS_OPTIONS_INIT;
                }
        }
  
-       init_db(git_dir, real_git_dir, option_template, GIT_HASH_UNKNOWN, NULL,
+       init_db(git_dir, real_git_dir, template_dir, GIT_HASH_UNKNOWN, NULL,
 -              INIT_DB_QUIET);
 +              do_not_override_repo_unix_permissions, INIT_DB_QUIET);
  
        if (real_git_dir) {
                free((char *)git_dir);
Simple merge
index 9b021ef026c28c97ece12b924833dff87c15a103,edb01aab70460fa9d3437e1b957cf03c1eaeaea9..272cddaafdd633f120acf3c6f8a87bf0a3fede38
@@@ -36,7 -36,9 +36,9 @@@ int cmd_upload_pack(int argc, const cha
        };
  
        packet_trace_identity("upload-pack");
 -      read_replace_refs = 0;
 +      disable_replace_refs();
+       /* TODO: This should use NO_LAZY_FETCH_ENVIRONMENT */
+       xsetenv("GIT_NO_LAZY_FETCH", "1", 0);
  
        argc = parse_options(argc, argv, prefix, options, upload_pack_usage, 0);
  
diff --cc ci/lib.sh
Simple merge
diff --cc config.c
index 3846a37be971c92153eb1ceeb65c6e612c8e173c,66fc088bd863642883e8de1fac4fd7cedb647fd4..cccda75a7885f5a5bab2dfac2fb04b011b78ab87
+++ b/config.c
@@@ -1558,8 -1596,19 +1558,19 @@@ static int git_default_core_config(cons
        if (!strcmp(var, "core.attributesfile"))
                return git_config_pathname(&git_attributes_file, var, value);
  
-       if (!strcmp(var, "core.hookspath"))
+       if (!strcmp(var, "core.hookspath")) {
 -              if (current_config_scope() == CONFIG_SCOPE_LOCAL &&
++              if (ctx->kvi && ctx->kvi->scope == CONFIG_SCOPE_LOCAL &&
+                   git_env_bool("GIT_CLONE_PROTECTION_ACTIVE", 0))
+                       die(_("active `core.hooksPath` found in the local "
+                             "repository config:\n\t%s\nFor security "
+                             "reasons, this is disallowed by default.\nIf "
+                             "this is intentional and the hook should "
+                             "actually be run, please\nrun the command "
+                             "again with "
+                             "`GIT_CLONE_PROTECTION_ACTIVE=false`"),
+                           value);
                return git_config_pathname(&git_hooks_path, var, value);
+       }
  
        if (!strcmp(var, "core.bare")) {
                is_bare_repository_cfg = git_config_bool(var, value);
diff --cc copy.c
index 23d84c6c1db554bcc7a33804d58821dfb3fb7634,1b4069cfea47b6b23ead6cdc26af850f3f95a9bb..3df156f6cea04b3eed87eb396ee14a968077b4ad
--- 1/copy.c
--- 2/copy.c
+++ b/copy.c
@@@ -1,6 -1,10 +1,9 @@@
  #include "git-compat-util.h"
  #include "copy.h"
  #include "path.h"
 -#include "wrapper.h"
+ #include "gettext.h"
+ #include "strbuf.h"
+ #include "abspath.h"
  
  int copy_fd(int ifd, int ofd)
  {
diff --cc dir.c
Simple merge
diff --cc dir.h
Simple merge
diff --cc entry.c
Simple merge
diff --cc fsck.c
Simple merge
diff --cc fsck.h
Simple merge
diff --cc hook.c
Simple merge
diff --cc http.c
Simple merge
diff --cc path.c
Simple merge
Simple merge
diff --cc read-cache.c
Simple merge
diff --cc remote-curl.c
index ef05752ca5738ea16d16d38c8ea9c65e2a9ea167,a0777e3f7a41527b4e8aa9ca9fbf48fe6d1dfe9e..dad241e89da12ff5f426fc05a9b9697635506830
@@@ -1,4 -1,6 +1,5 @@@
  #include "git-compat-util.h"
 -#include "alloc.h"
+ #include "git-curl-compat.h"
  #include "config.h"
  #include "environment.h"
  #include "gettext.h"
diff --cc repository.c
Simple merge
diff --cc setup.c
index 2e607632dbde807ae76566e282d8f122b1642cda,84324e35c6967f8ae41ff62d5766f1864f57b7be..881935f0956bec94e7c1bc7a897de60fcaac528d
+++ b/setup.c
@@@ -16,7 -12,8 +16,8 @@@
  #include "promisor-remote.h"
  #include "quote.h"
  #include "trace2.h"
 -#include "wrapper.h"
 +#include "worktree.h"
+ #include "exec-cmd.h"
  
  static int inside_git_dir = -1;
  static int inside_work_tree = -1;
@@@ -1179,9 -1173,28 +1180,30 @@@ static int ensure_valid_ownership(cons
        return data.is_safe;
  }
  
 -static int allowed_bare_repo_cb(const char *key, const char *value, void *d)
+ void die_upon_dubious_ownership(const char *gitfile, const char *worktree,
+                               const char *gitdir)
+ {
+       struct strbuf report = STRBUF_INIT, quoted = STRBUF_INIT;
+       const char *path;
+       if (ensure_valid_ownership(gitfile, worktree, gitdir, &report))
+               return;
+       strbuf_complete(&report, '\n');
+       path = gitfile ? gitfile : gitdir;
+       sq_quote_buf_pretty(&quoted, path);
+       die(_("detected dubious ownership in repository at '%s'\n"
+             "%s"
+             "To add an exception for this directory, call:\n"
+             "\n"
+             "\tgit config --global --add safe.directory %s"),
+           path, report.buf, quoted.buf);
+ }
 +static int allowed_bare_repo_cb(const char *key, const char *value,
 +                              const struct config_context *ctx UNUSED,
 +                              void *d)
  {
        enum allowed_bare_repo *allowed_bare_repo = d;
  
@@@ -1706,490 -1730,56 +1728,531 @@@ int daemonize(void
  #endif
  }
  
 -#ifndef DEFAULT_GIT_TEMPLATE_DIR
 -#define DEFAULT_GIT_TEMPLATE_DIR "/usr/share/git-core/templates"
 -#endif
 -
+ struct template_dir_cb_data {
+       char *path;
+       int initialized;
+ };
 -static int template_dir_cb(const char *key, const char *value, void *d)
++static int template_dir_cb(const char *key, const char *value,
++                         const struct config_context *ctx, void *d)
+ {
+       struct template_dir_cb_data *data = d;
+       if (strcmp(key, "init.templatedir"))
+               return 0;
+       if (!value) {
+               data->path = NULL;
+       } else {
+               char *path = NULL;
+               FREE_AND_NULL(data->path);
+               if (!git_config_pathname((const char **)&path, key, value))
+                       data->path = path ? path : xstrdup(value);
+       }
+       return 0;
+ }
+ const char *get_template_dir(const char *option_template)
+ {
+       const char *template_dir = option_template;
+       if (!template_dir)
+               template_dir = getenv(TEMPLATE_DIR_ENVIRONMENT);
+       if (!template_dir) {
+               static struct template_dir_cb_data data;
+               if (!data.initialized) {
+                       git_protected_config(template_dir_cb, &data);
+                       data.initialized = 1;
+               }
+               template_dir = data.path;
+       }
+       if (!template_dir) {
+               static char *dir;
+               if (!dir)
+                       dir = system_path(DEFAULT_GIT_TEMPLATE_DIR);
+               template_dir = dir;
+       }
+       return template_dir;
+ }
++
 +#ifdef NO_TRUSTABLE_FILEMODE
 +#define TEST_FILEMODE 0
 +#else
 +#define TEST_FILEMODE 1
 +#endif
 +
 +#define GIT_DEFAULT_HASH_ENVIRONMENT "GIT_DEFAULT_HASH"
 +
 +static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
 +                           DIR *dir)
 +{
 +      size_t path_baselen = path->len;
 +      size_t template_baselen = template_path->len;
 +      struct dirent *de;
 +
 +      /* Note: if ".git/hooks" file exists in the repository being
 +       * re-initialized, /etc/core-git/templates/hooks/update would
 +       * cause "git init" to fail here.  I think this is sane but
 +       * it means that the set of templates we ship by default, along
 +       * with the way the namespace under .git/ is organized, should
 +       * be really carefully chosen.
 +       */
 +      safe_create_dir(path->buf, 1);
 +      while ((de = readdir(dir)) != NULL) {
 +              struct stat st_git, st_template;
 +              int exists = 0;
 +
 +              strbuf_setlen(path, path_baselen);
 +              strbuf_setlen(template_path, template_baselen);
 +
 +              if (de->d_name[0] == '.')
 +                      continue;
 +              strbuf_addstr(path, de->d_name);
 +              strbuf_addstr(template_path, de->d_name);
 +              if (lstat(path->buf, &st_git)) {
 +                      if (errno != ENOENT)
 +                              die_errno(_("cannot stat '%s'"), path->buf);
 +              }
 +              else
 +                      exists = 1;
 +
 +              if (lstat(template_path->buf, &st_template))
 +                      die_errno(_("cannot stat template '%s'"), template_path->buf);
 +
 +              if (S_ISDIR(st_template.st_mode)) {
 +                      DIR *subdir = opendir(template_path->buf);
 +                      if (!subdir)
 +                              die_errno(_("cannot opendir '%s'"), template_path->buf);
 +                      strbuf_addch(path, '/');
 +                      strbuf_addch(template_path, '/');
 +                      copy_templates_1(path, template_path, subdir);
 +                      closedir(subdir);
 +              }
 +              else if (exists)
 +                      continue;
 +              else if (S_ISLNK(st_template.st_mode)) {
 +                      struct strbuf lnk = STRBUF_INIT;
 +                      if (strbuf_readlink(&lnk, template_path->buf,
 +                                          st_template.st_size) < 0)
 +                              die_errno(_("cannot readlink '%s'"), template_path->buf);
 +                      if (symlink(lnk.buf, path->buf))
 +                              die_errno(_("cannot symlink '%s' '%s'"),
 +                                        lnk.buf, path->buf);
 +                      strbuf_release(&lnk);
 +              }
 +              else if (S_ISREG(st_template.st_mode)) {
 +                      if (copy_file(path->buf, template_path->buf, st_template.st_mode))
 +                              die_errno(_("cannot copy '%s' to '%s'"),
 +                                        template_path->buf, path->buf);
 +              }
 +              else
 +                      error(_("ignoring template %s"), template_path->buf);
 +      }
 +}
 +
- static void copy_templates(const char *template_dir, const char *init_template_dir)
++static void copy_templates(const char *option_template)
 +{
++      const char *template_dir = get_template_dir(option_template);
 +      struct strbuf path = STRBUF_INIT;
 +      struct strbuf template_path = STRBUF_INIT;
 +      size_t template_len;
 +      struct repository_format template_format = REPOSITORY_FORMAT_INIT;
 +      struct strbuf err = STRBUF_INIT;
 +      DIR *dir;
 +      char *to_free = NULL;
 +
-       if (!template_dir)
-               template_dir = getenv(TEMPLATE_DIR_ENVIRONMENT);
-       if (!template_dir)
-               template_dir = init_template_dir;
-       if (!template_dir)
-               template_dir = to_free = system_path(DEFAULT_GIT_TEMPLATE_DIR);
-       if (!template_dir[0]) {
-               free(to_free);
++      if (!template_dir || !*template_dir)
 +              return;
-       }
 +
 +      strbuf_addstr(&template_path, template_dir);
 +      strbuf_complete(&template_path, '/');
 +      template_len = template_path.len;
 +
 +      dir = opendir(template_path.buf);
 +      if (!dir) {
 +              warning(_("templates not found in %s"), template_dir);
 +              goto free_return;
 +      }
 +
 +      /* Make sure that template is from the correct vintage */
 +      strbuf_addstr(&template_path, "config");
 +      read_repository_format(&template_format, template_path.buf);
 +      strbuf_setlen(&template_path, template_len);
 +
 +      /*
 +       * No mention of version at all is OK, but anything else should be
 +       * verified.
 +       */
 +      if (template_format.version >= 0 &&
 +          verify_repository_format(&template_format, &err) < 0) {
 +              warning(_("not copying templates from '%s': %s"),
 +                        template_dir, err.buf);
 +              strbuf_release(&err);
 +              goto close_free_return;
 +      }
 +
 +      strbuf_addstr(&path, get_git_common_dir());
 +      strbuf_complete(&path, '/');
 +      copy_templates_1(&path, &template_path, dir);
 +close_free_return:
 +      closedir(dir);
 +free_return:
 +      free(to_free);
 +      strbuf_release(&path);
 +      strbuf_release(&template_path);
 +      clear_repository_format(&template_format);
 +}
 +
 +/*
 + * If the git_dir is not directly inside the working tree, then git will not
 + * find it by default, and we need to set the worktree explicitly.
 + */
 +static int needs_work_tree_config(const char *git_dir, const char *work_tree)
 +{
 +      if (!strcmp(work_tree, "/") && !strcmp(git_dir, "/.git"))
 +              return 0;
 +      if (skip_prefix(git_dir, work_tree, &git_dir) &&
 +          !strcmp(git_dir, "/.git"))
 +              return 0;
 +      return 1;
 +}
 +
 +void initialize_repository_version(int hash_algo, int reinit)
 +{
 +      char repo_version_string[10];
 +      int repo_version = GIT_REPO_VERSION;
 +
 +      if (hash_algo != GIT_HASH_SHA1)
 +              repo_version = GIT_REPO_VERSION_READ;
 +
 +      /* This forces creation of new config file */
 +      xsnprintf(repo_version_string, sizeof(repo_version_string),
 +                "%d", repo_version);
 +      git_config_set("core.repositoryformatversion", repo_version_string);
 +
 +      if (hash_algo != GIT_HASH_SHA1)
 +              git_config_set("extensions.objectformat",
 +                             hash_algos[hash_algo].name);
 +      else if (reinit)
 +              git_config_set_gently("extensions.objectformat", NULL);
 +}
 +
 +static int create_default_files(const char *template_path,
 +                              const char *original_git_dir,
 +                              const char *initial_branch,
 +                              const struct repository_format *fmt,
 +                              int prev_bare_repository,
 +                              int init_shared_repository,
 +                              int quiet)
 +{
 +      struct stat st1;
 +      struct strbuf buf = STRBUF_INIT;
 +      char *path;
 +      char junk[2];
 +      int reinit;
 +      int filemode;
 +      struct strbuf err = STRBUF_INIT;
-       const char *init_template_dir = NULL;
 +      const char *work_tree = get_git_work_tree();
 +
 +      /*
 +       * First copy the templates -- we might have the default
 +       * config file there, in which case we would want to read
 +       * from it after installing.
 +       *
 +       * Before reading that config, we also need to clear out any cached
 +       * values (since we've just potentially changed what's available on
 +       * disk).
 +       */
-       git_config_get_pathname("init.templatedir", &init_template_dir);
-       copy_templates(template_path, init_template_dir);
-       free((char *)init_template_dir);
++      copy_templates(template_path);
 +      git_config_clear();
 +      reset_shared_repository();
 +      git_config(git_default_config, NULL);
 +
 +      /*
 +       * We must make sure command-line options continue to override any
 +       * values we might have just re-read from the config.
 +       */
 +      if (init_shared_repository != -1)
 +              set_shared_repository(init_shared_repository);
 +      /*
 +       * TODO: heed core.bare from config file in templates if no
 +       *       command-line override given
 +       */
 +      is_bare_repository_cfg = prev_bare_repository || !work_tree;
 +      /* TODO (continued):
 +       *
 +       * Unfortunately, the line above is equivalent to
 +       *    is_bare_repository_cfg = !work_tree;
 +       * which ignores the config entirely even if no `--[no-]bare`
 +       * command line option was present.
 +       *
 +       * To see why, note that before this function, there was this call:
 +       *    prev_bare_repository = is_bare_repository()
 +       * expanding the right hand side:
 +       *                 = is_bare_repository_cfg && !get_git_work_tree()
 +       *                 = is_bare_repository_cfg && !work_tree
 +       * note that the last simplification above is valid because nothing
 +       * calls repo_init() or set_git_work_tree() between any of the
 +       * relevant calls in the code, and thus the !get_git_work_tree()
 +       * calls will return the same result each time.  So, what we are
 +       * interested in computing is the right hand side of the line of
 +       * code just above this comment:
 +       *     prev_bare_repository || !work_tree
 +       *        = is_bare_repository_cfg && !work_tree || !work_tree
 +       *        = !work_tree
 +       * because "A && !B || !B == !B" for all boolean values of A & B.
 +       */
 +
 +      /*
 +       * We would have created the above under user's umask -- under
 +       * shared-repository settings, we would need to fix them up.
 +       */
 +      if (get_shared_repository()) {
 +              adjust_shared_perm(get_git_dir());
 +      }
 +
 +      /*
 +       * We need to create a "refs" dir in any case so that older
 +       * versions of git can tell that this is a repository.
 +       */
 +      safe_create_dir(git_path("refs"), 1);
 +      adjust_shared_perm(git_path("refs"));
 +
 +      if (refs_init_db(&err))
 +              die("failed to set up refs db: %s", err.buf);
 +
 +      /*
 +       * Point the HEAD symref to the initial branch with if HEAD does
 +       * not yet exist.
 +       */
 +      path = git_path_buf(&buf, "HEAD");
 +      reinit = (!access(path, R_OK)
 +                || readlink(path, junk, sizeof(junk)-1) != -1);
 +      if (!reinit) {
 +              char *ref;
 +
 +              if (!initial_branch)
 +                      initial_branch = git_default_branch_name(quiet);
 +
 +              ref = xstrfmt("refs/heads/%s", initial_branch);
 +              if (check_refname_format(ref, 0) < 0)
 +                      die(_("invalid initial branch name: '%s'"),
 +                          initial_branch);
 +
 +              if (create_symref("HEAD", ref, NULL) < 0)
 +                      exit(1);
 +              free(ref);
 +      }
 +
 +      initialize_repository_version(fmt->hash_algo, 0);
 +
 +      /* Check filemode trustability */
 +      path = git_path_buf(&buf, "config");
 +      filemode = TEST_FILEMODE;
 +      if (TEST_FILEMODE && !lstat(path, &st1)) {
 +              struct stat st2;
 +              filemode = (!chmod(path, st1.st_mode ^ S_IXUSR) &&
 +                              !lstat(path, &st2) &&
 +                              st1.st_mode != st2.st_mode &&
 +                              !chmod(path, st1.st_mode));
 +              if (filemode && !reinit && (st1.st_mode & S_IXUSR))
 +                      filemode = 0;
 +      }
 +      git_config_set("core.filemode", filemode ? "true" : "false");
 +
 +      if (is_bare_repository())
 +              git_config_set("core.bare", "true");
 +      else {
 +              git_config_set("core.bare", "false");
 +              /* allow template config file to override the default */
 +              if (log_all_ref_updates == LOG_REFS_UNSET)
 +                      git_config_set("core.logallrefupdates", "true");
 +              if (needs_work_tree_config(original_git_dir, work_tree))
 +                      git_config_set("core.worktree", work_tree);
 +      }
 +
 +      if (!reinit) {
 +              /* Check if symlink is supported in the work tree */
 +              path = git_path_buf(&buf, "tXXXXXX");
 +              if (!close(xmkstemp(path)) &&
 +                  !unlink(path) &&
 +                  !symlink("testing", path) &&
 +                  !lstat(path, &st1) &&
 +                  S_ISLNK(st1.st_mode))
 +                      unlink(path); /* good */
 +              else
 +                      git_config_set("core.symlinks", "false");
 +
 +              /* Check if the filesystem is case-insensitive */
 +              path = git_path_buf(&buf, "CoNfIg");
 +              if (!access(path, F_OK))
 +                      git_config_set("core.ignorecase", "true");
 +              probe_utf8_pathname_composition();
 +      }
 +
 +      strbuf_release(&buf);
 +      return reinit;
 +}
 +
 +static void create_object_directory(void)
 +{
 +      struct strbuf path = STRBUF_INIT;
 +      size_t baselen;
 +
 +      strbuf_addstr(&path, get_object_directory());
 +      baselen = path.len;
 +
 +      safe_create_dir(path.buf, 1);
 +
 +      strbuf_setlen(&path, baselen);
 +      strbuf_addstr(&path, "/pack");
 +      safe_create_dir(path.buf, 1);
 +
 +      strbuf_setlen(&path, baselen);
 +      strbuf_addstr(&path, "/info");
 +      safe_create_dir(path.buf, 1);
 +
 +      strbuf_release(&path);
 +}
 +
 +static void separate_git_dir(const char *git_dir, const char *git_link)
 +{
 +      struct stat st;
 +
 +      if (!stat(git_link, &st)) {
 +              const char *src;
 +
 +              if (S_ISREG(st.st_mode))
 +                      src = read_gitfile(git_link);
 +              else if (S_ISDIR(st.st_mode))
 +                      src = git_link;
 +              else
 +                      die(_("unable to handle file type %d"), (int)st.st_mode);
 +
 +              if (rename(src, git_dir))
 +                      die_errno(_("unable to move %s to %s"), src, git_dir);
 +              repair_worktrees(NULL, NULL);
 +      }
 +
 +      write_file(git_link, "gitdir: %s", git_dir);
 +}
 +
 +static void validate_hash_algorithm(struct repository_format *repo_fmt, int hash)
 +{
 +      const char *env = getenv(GIT_DEFAULT_HASH_ENVIRONMENT);
 +      /*
 +       * If we already have an initialized repo, don't allow the user to
 +       * specify a different algorithm, as that could cause corruption.
 +       * Otherwise, if the user has specified one on the command line, use it.
 +       */
 +      if (repo_fmt->version >= 0 && hash != GIT_HASH_UNKNOWN && hash != repo_fmt->hash_algo)
 +              die(_("attempt to reinitialize repository with different hash"));
 +      else if (hash != GIT_HASH_UNKNOWN)
 +              repo_fmt->hash_algo = hash;
 +      else if (env) {
 +              int env_algo = hash_algo_by_name(env);
 +              if (env_algo == GIT_HASH_UNKNOWN)
 +                      die(_("unknown hash algorithm '%s'"), env);
 +              repo_fmt->hash_algo = env_algo;
 +      }
 +}
 +
 +int init_db(const char *git_dir, const char *real_git_dir,
 +          const char *template_dir, int hash, const char *initial_branch,
 +          int init_shared_repository, unsigned int flags)
 +{
 +      int reinit;
 +      int exist_ok = flags & INIT_DB_EXIST_OK;
 +      char *original_git_dir = real_pathdup(git_dir, 1);
 +      struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;
 +      int prev_bare_repository;
 +
 +      if (real_git_dir) {
 +              struct stat st;
 +
 +              if (!exist_ok && !stat(git_dir, &st))
 +                      die(_("%s already exists"), git_dir);
 +
 +              if (!exist_ok && !stat(real_git_dir, &st))
 +                      die(_("%s already exists"), real_git_dir);
 +
 +              set_git_dir(real_git_dir, 1);
 +              git_dir = get_git_dir();
 +              separate_git_dir(git_dir, original_git_dir);
 +      }
 +      else {
 +              set_git_dir(git_dir, 1);
 +              git_dir = get_git_dir();
 +      }
 +      startup_info->have_repository = 1;
 +
 +      /* Ensure `core.hidedotfiles` is processed */
 +      git_config(platform_core_config, NULL);
 +
 +      safe_create_dir(git_dir, 0);
 +
 +      prev_bare_repository = is_bare_repository();
 +
 +      /* Check to see if the repository version is right.
 +       * Note that a newly created repository does not have
 +       * config file, so this will not fail.  What we are catching
 +       * is an attempt to reinitialize new repository with an old tool.
 +       */
 +      check_repository_format(&repo_fmt);
 +
 +      validate_hash_algorithm(&repo_fmt, hash);
 +
 +      reinit = create_default_files(template_dir, original_git_dir,
 +                                    initial_branch, &repo_fmt,
 +                                    prev_bare_repository,
 +                                    init_shared_repository,
 +                                    flags & INIT_DB_QUIET);
 +      if (reinit && initial_branch)
 +              warning(_("re-init: ignored --initial-branch=%s"),
 +                      initial_branch);
 +
 +      create_object_directory();
 +
 +      if (get_shared_repository()) {
 +              char buf[10];
 +              /* We do not spell "group" and such, so that
 +               * the configuration can be read by older version
 +               * of git. Note, we use octal numbers for new share modes,
 +               * and compatibility values for PERM_GROUP and
 +               * PERM_EVERYBODY.
 +               */
 +              if (get_shared_repository() < 0)
 +                      /* force to the mode value */
 +                      xsnprintf(buf, sizeof(buf), "0%o", -get_shared_repository());
 +              else if (get_shared_repository() == PERM_GROUP)
 +                      xsnprintf(buf, sizeof(buf), "%d", OLD_PERM_GROUP);
 +              else if (get_shared_repository() == PERM_EVERYBODY)
 +                      xsnprintf(buf, sizeof(buf), "%d", OLD_PERM_EVERYBODY);
 +              else
 +                      BUG("invalid value for shared_repository");
 +              git_config_set("core.sharedrepository", buf);
 +              git_config_set("receive.denyNonFastforwards", "true");
 +      }
 +
 +      if (!(flags & INIT_DB_QUIET)) {
 +              int len = strlen(git_dir);
 +
 +              if (reinit)
 +                      printf(get_shared_repository()
 +                             ? _("Reinitialized existing shared Git repository in %s%s\n")
 +                             : _("Reinitialized existing Git repository in %s%s\n"),
 +                             git_dir, len && git_dir[len-1] != '/' ? "/" : "");
 +              else
 +                      printf(get_shared_repository()
 +                             ? _("Initialized empty shared Git repository in %s%s\n")
 +                             : _("Initialized empty Git repository in %s%s\n"),
 +                             git_dir, len && git_dir[len-1] != '/' ? "/" : "");
 +      }
 +
 +      free(original_git_dir);
 +      return 0;
 +}
diff --cc setup.h
index b48cf1c43b5a559057a2e92d2fe4250c15255344,e7708f573904729eae7b00b3e3c3ea5ccb8ac7a0..01a6ad77358f135c9e7333ac3e25aa82db1b84f7
+++ b/setup.h
@@@ -41,30 -41,19 +41,42 @@@ const char *read_gitfile_gently(const c
  const char *resolve_gitdir_gently(const char *suspect, int *return_error_code);
  #define resolve_gitdir(path) resolve_gitdir_gently((path), NULL)
  
+ /*
+  * Check if a repository is safe and die if it is not, by verifying the
+  * ownership of the worktree (if any), the git directory, and the gitfile (if
+  * any).
+  *
+  * Exemptions for known-safe repositories can be added via `safe.directory`
+  * config settings; for non-bare repositories, their worktree needs to be
+  * added, for bare ones their git directory.
+  */
+ void die_upon_dubious_ownership(const char *gitfile, const char *worktree,
+                               const char *gitdir);
  void setup_work_tree(void);
 +
 +/*
 + * discover_git_directory_reason() is similar to discover_git_directory(),
 + * except it returns an enum value instead. It is important to note that
 + * a zero-valued return here is actually GIT_DIR_NONE, which is different
 + * from discover_git_directory.
 + */
 +enum discovery_result {
 +      GIT_DIR_EXPLICIT = 1,
 +      GIT_DIR_DISCOVERED = 2,
 +      GIT_DIR_BARE = 3,
 +      /* these are errors */
 +      GIT_DIR_HIT_CEILING = -1,
 +      GIT_DIR_HIT_MOUNT_POINT = -2,
 +      GIT_DIR_INVALID_GITFILE = -3,
 +      GIT_DIR_INVALID_OWNERSHIP = -4,
 +      GIT_DIR_DISALLOWED_BARE = -5,
 +      GIT_DIR_INVALID_FORMAT = -6,
 +      GIT_DIR_CWD_FAILURE = -7,
 +};
 +enum discovery_result discover_git_directory_reason(struct strbuf *commondir,
 +                                                  struct strbuf *gitdir);
 +
  /*
   * Find the commondir and gitdir of the repository that contains the current
   * working directory, without changing the working directory or other global
@@@ -169,15 -152,8 +181,17 @@@ int verify_repository_format(const stru
   */
  void check_repository_format(struct repository_format *fmt);
  
+ const char *get_template_dir(const char *option_template);
 +#define INIT_DB_QUIET 0x0001
 +#define INIT_DB_EXIST_OK 0x0002
 +
 +int init_db(const char *git_dir, const char *real_git_dir,
 +          const char *template_dir, int hash_algo,
 +          const char *initial_branch, int init_shared_repository,
 +          unsigned int flags);
 +void initialize_repository_version(int hash_algo, int reinit);
 +
  /*
   * NOTE NOTE NOTE!!
   *
diff --cc submodule.c
Simple merge
Simple merge
Simple merge
diff --cc t/t1450-fsck.sh
Simple merge
diff --cc t/t1800-hook.sh
Simple merge
Simple merge
Simple merge
index 00651c25cb4089a4d7334ad00f6ebc8e51bafb0f,dae87090e02ea57352c7f575a069e1bc402c9851..fb82f760c3d7759198aa9baa392a1d05fedb684b
@@@ -1179,27 -1179,52 +1179,75 @@@ test_expect_success 'submodule update -
        test_cmp expect.err actual.err
  '
  
 +add_submodule_commit_and_validate () {
 +      HASH=$(git rev-parse HEAD) &&
 +      git update-index --add --cacheinfo 160000,$HASH,sub &&
 +      git commit -m "create submodule" &&
 +      echo "160000 commit $HASH       sub" >expect &&
 +      git ls-tree HEAD -- sub >actual &&
 +      test_cmp expect actual
 +}
 +
 +test_expect_success 'commit with staged submodule change' '
 +      add_submodule_commit_and_validate
 +'
 +
 +test_expect_success 'commit with staged submodule change with ignoreSubmodules dirty' '
 +      test_config diff.ignoreSubmodules dirty &&
 +      add_submodule_commit_and_validate
 +'
 +
 +test_expect_success 'commit with staged submodule change with ignoreSubmodules all' '
 +      test_config diff.ignoreSubmodules all &&
 +      add_submodule_commit_and_validate
 +'
 +
+ test_expect_success CASE_INSENSITIVE_FS,SYMLINKS \
+       'submodule paths must not follow symlinks' '
+       # This is only needed because we want to run this in a self-contained
+       # test without having to spin up an HTTP server; However, it would not
+       # be needed in a real-world scenario where the submodule is simply
+       # hosted on a public site.
+       test_config_global protocol.file.allow always &&
+       # Make sure that Git tries to use symlinks on Windows
+       test_config_global core.symlinks true &&
+       tell_tale_path="$PWD/tell.tale" &&
+       git init hook &&
+       (
+               cd hook &&
+               mkdir -p y/hooks &&
+               write_script y/hooks/post-checkout <<-EOF &&
+               echo HOOK-RUN >&2
+               echo hook-run >"$tell_tale_path"
+               EOF
+               git add y/hooks/post-checkout &&
+               test_tick &&
+               git commit -m post-checkout
+       ) &&
+       hook_repo_path="$(pwd)/hook" &&
+       git init captain &&
+       (
+               cd captain &&
+               git submodule add --name x/y "$hook_repo_path" A/modules/x &&
+               test_tick &&
+               git commit -m add-submodule &&
+               printf .git >dotgit.txt &&
+               git hash-object -w --stdin <dotgit.txt >dot-git.hash &&
+               printf "120000 %s 0\ta\n" "$(cat dot-git.hash)" >index.info &&
+               git update-index --index-info <index.info &&
+               test_tick &&
+               git commit -m add-symlink
+       ) &&
+       test_path_is_missing "$tell_tale_path" &&
+       git clone --recursive captain hooked 2>err &&
+       ! grep HOOK-RUN err &&
+       test_path_is_missing "$tell_tale_path"
+ '
  test_done