Merge branch 'fa/p4-error' into maint-2.45

P4 update.

* fa/p4-error:
  git-p4: show Perforce error to the user
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 3428773..8922bd7 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -284,8 +284,7 @@
             cc: clang
             pool: macos-13
           - jobname: osx-gcc
-            cc: gcc
-            cc_package: gcc-13
+            cc: gcc-13
             pool: macos-13
           - jobname: linux-gcc-default
             cc: gcc
diff --git a/Documentation/RelNotes/2.39.4.txt b/Documentation/RelNotes/2.39.4.txt
new file mode 100644
index 0000000..7f54521
--- /dev/null
+++ b/Documentation/RelNotes/2.39.4.txt
@@ -0,0 +1,79 @@
+Git v2.39.4 Release Notes
+=========================
+
+This addresses the security issues CVE-2024-32002, CVE-2024-32004,
+CVE-2024-32020 and CVE-2024-32021.
+
+This release also backports fixes necessary to let the CI builds pass
+successfully.
+
+Fixes since v2.39.3
+-------------------
+
+ * CVE-2024-32002:
+
+   Recursive clones on case-insensitive filesystems that support symbolic
+   links are susceptible to case confusion that can be exploited to
+   execute just-cloned code during the clone operation.
+
+ * CVE-2024-32004:
+
+   Repositories can be configured to execute arbitrary code during local
+   clones. To address this, the ownership checks introduced in v2.30.3
+   are now extended to cover cloning local repositories.
+
+ * CVE-2024-32020:
+
+   Local clones may end up hardlinking files into the target repository's
+   object database when source and target repository reside on the same
+   disk. If the source repository is owned by a different user, then
+   those hardlinked files may be rewritten at any point in time by the
+   untrusted user.
+
+ * CVE-2024-32021:
+
+   When cloning a local source repository that contains symlinks via the
+   filesystem, Git may create hardlinks to arbitrary user-readable files
+   on the same filesystem as the target repository in the objects/
+   directory.
+
+ * CVE-2024-32465:
+
+   It is supposed to be safe to clone untrusted repositories, even those
+   unpacked from zip archives or tarballs originating from untrusted
+   sources, but Git can be tricked to run arbitrary code as part of the
+   clone.
+
+ * Defense-in-depth: submodule: require the submodule path to contain
+   directories only.
+
+ * Defense-in-depth: clone: when symbolic links collide with directories, keep
+   the latter.
+
+ * Defense-in-depth: clone: prevent hooks from running during a clone.
+
+ * Defense-in-depth: core.hooksPath: add some protection while cloning.
+
+ * Defense-in-depth: fsck: warn about symlink pointing inside a gitdir.
+
+ * Various fix-ups on HTTP tests.
+
+ * Test update.
+
+ * HTTP Header redaction code has been adjusted for a newer version of
+   cURL library that shows its traces differently from earlier
+   versions.
+
+ * Fix was added to work around a regression in libcURL 8.7.0 (which has
+   already been fixed in their tip of the tree).
+
+ * Replace macos-12 used at GitHub CI with macos-13.
+
+ * ci(linux-asan/linux-ubsan): let's save some time
+
+ * Tests with LSan from time to time seem to emit harmless message that makes
+   our tests unnecessarily flakey; we work it around by filtering the
+   uninteresting output.
+
+ * Update GitHub Actions jobs to avoid warnings against using deprecated
+   version of Node.js.
diff --git a/Documentation/RelNotes/2.39.5.txt b/Documentation/RelNotes/2.39.5.txt
new file mode 100644
index 0000000..97c0185
--- /dev/null
+++ b/Documentation/RelNotes/2.39.5.txt
@@ -0,0 +1,26 @@
+Git v2.39.5 Release Notes
+=========================
+
+In preparing security fixes for four CVEs, we made overly aggressive
+"defense in depth" changes that broke legitimate use cases like 'git
+lfs' and 'git annex.'  This release is to revert these misguided, if
+well-intentioned, changes that were shipped in 2.39.4 and were not
+direct security fixes.
+
+Jeff King (5):
+      send-email: drop FakeTerm hack
+      send-email: avoid creating more than one Term::ReadLine object
+      ci: drop mention of BREW_INSTALL_PACKAGES variable
+      ci: avoid bare "gcc" for osx-gcc job
+      ci: stop installing "gcc-13" for osx-gcc
+
+Johannes Schindelin (6):
+      hook: plug a new memory leak
+      init: use the correct path of the templates directory again
+      Revert "core.hooksPath: add some protection while cloning"
+      tests: verify that `clone -c core.hooksPath=/dev/null` works again
+      clone: drop the protections where hooks aren't run
+      Revert "Add a helper function to compare file contents"
+
+Junio C Hamano (1):
+      Revert "fsck: warn about symlink pointing inside a gitdir"
diff --git a/Documentation/RelNotes/2.40.2.txt b/Documentation/RelNotes/2.40.2.txt
new file mode 100644
index 0000000..646a2cc
--- /dev/null
+++ b/Documentation/RelNotes/2.40.2.txt
@@ -0,0 +1,7 @@
+Git v2.40.2 Release Notes
+=========================
+
+This release merges up the fix that appears in v2.39.4 to address
+the security issues CVE-2024-32002, CVE-2024-32004, CVE-2024-32020,
+CVE-2024-32021 and CVE-2024-32465; see the release notes for that
+version for details.
diff --git a/Documentation/RelNotes/2.40.3.txt b/Documentation/RelNotes/2.40.3.txt
new file mode 100644
index 0000000..6ca088e
--- /dev/null
+++ b/Documentation/RelNotes/2.40.3.txt
@@ -0,0 +1,26 @@
+Git v2.40.3 Release Notes
+=========================
+
+In preparing security fixes for four CVEs, we made overly aggressive
+"defense in depth" changes that broke legitimate use cases like 'git
+lfs' and 'git annex.'  This release is to revert these misguided, if
+well-intentioned, changes that were shipped in 2.40.2 and were not
+direct security fixes.
+
+Jeff King (5):
+      send-email: drop FakeTerm hack
+      send-email: avoid creating more than one Term::ReadLine object
+      ci: drop mention of BREW_INSTALL_PACKAGES variable
+      ci: avoid bare "gcc" for osx-gcc job
+      ci: stop installing "gcc-13" for osx-gcc
+
+Johannes Schindelin (6):
+      hook: plug a new memory leak
+      init: use the correct path of the templates directory again
+      Revert "core.hooksPath: add some protection while cloning"
+      tests: verify that `clone -c core.hooksPath=/dev/null` works again
+      clone: drop the protections where hooks aren't run
+      Revert "Add a helper function to compare file contents"
+
+Junio C Hamano (1):
+      Revert "fsck: warn about symlink pointing inside a gitdir"
diff --git a/Documentation/RelNotes/2.41.1.txt b/Documentation/RelNotes/2.41.1.txt
new file mode 100644
index 0000000..9fb4c21
--- /dev/null
+++ b/Documentation/RelNotes/2.41.1.txt
@@ -0,0 +1,7 @@
+Git v2.41.1 Release Notes
+=========================
+
+This release merges up the fix that appears in v2.39.4 and v2.40.2
+to address the security issues CVE-2024-32002, CVE-2024-32004,
+CVE-2024-32020, CVE-2024-32021 and CVE-2024-32465; see the release
+notes for these versions for details.
diff --git a/Documentation/RelNotes/2.41.2.txt b/Documentation/RelNotes/2.41.2.txt
new file mode 100644
index 0000000..f94afde
--- /dev/null
+++ b/Documentation/RelNotes/2.41.2.txt
@@ -0,0 +1,26 @@
+Git v2.41.2 Release Notes
+=========================
+
+In preparing security fixes for four CVEs, we made overly aggressive
+"defense in depth" changes that broke legitimate use cases like 'git
+lfs' and 'git annex.'  This release is to revert these misguided, if
+well-intentioned, changes that were shipped in 2.41.1 and were not
+direct security fixes.
+
+Jeff King (5):
+      send-email: drop FakeTerm hack
+      send-email: avoid creating more than one Term::ReadLine object
+      ci: drop mention of BREW_INSTALL_PACKAGES variable
+      ci: avoid bare "gcc" for osx-gcc job
+      ci: stop installing "gcc-13" for osx-gcc
+
+Johannes Schindelin (6):
+      hook: plug a new memory leak
+      init: use the correct path of the templates directory again
+      Revert "core.hooksPath: add some protection while cloning"
+      tests: verify that `clone -c core.hooksPath=/dev/null` works again
+      clone: drop the protections where hooks aren't run
+      Revert "Add a helper function to compare file contents"
+
+Junio C Hamano (1):
+      Revert "fsck: warn about symlink pointing inside a gitdir"
diff --git a/Documentation/RelNotes/2.42.2.txt b/Documentation/RelNotes/2.42.2.txt
new file mode 100644
index 0000000..dbf761a
--- /dev/null
+++ b/Documentation/RelNotes/2.42.2.txt
@@ -0,0 +1,7 @@
+Git v2.42.2 Release Notes
+=========================
+
+This release merges up the fix that appears in v2.39.4, v2.40.2
+and v2.41.1 to address the security issues CVE-2024-32002,
+CVE-2024-32004, CVE-2024-32020, CVE-2024-32021 and CVE-2024-32465;
+see the release notes for these versions for details.
diff --git a/Documentation/RelNotes/2.42.3.txt b/Documentation/RelNotes/2.42.3.txt
new file mode 100644
index 0000000..bfe3ba5
--- /dev/null
+++ b/Documentation/RelNotes/2.42.3.txt
@@ -0,0 +1,26 @@
+Git v2.42.3 Release Notes
+=========================
+
+In preparing security fixes for four CVEs, we made overly aggressive
+"defense in depth" changes that broke legitimate use cases like 'git
+lfs' and 'git annex.'  This release is to revert these misguided, if
+well-intentioned, changes that were shipped in 2.42.2 and were not
+direct security fixes.
+
+Jeff King (5):
+      send-email: drop FakeTerm hack
+      send-email: avoid creating more than one Term::ReadLine object
+      ci: drop mention of BREW_INSTALL_PACKAGES variable
+      ci: avoid bare "gcc" for osx-gcc job
+      ci: stop installing "gcc-13" for osx-gcc
+
+Johannes Schindelin (6):
+      hook: plug a new memory leak
+      init: use the correct path of the templates directory again
+      Revert "core.hooksPath: add some protection while cloning"
+      tests: verify that `clone -c core.hooksPath=/dev/null` works again
+      clone: drop the protections where hooks aren't run
+      Revert "Add a helper function to compare file contents"
+
+Junio C Hamano (1):
+      Revert "fsck: warn about symlink pointing inside a gitdir"
diff --git a/Documentation/RelNotes/2.43.4.txt b/Documentation/RelNotes/2.43.4.txt
new file mode 100644
index 0000000..0a84251
--- /dev/null
+++ b/Documentation/RelNotes/2.43.4.txt
@@ -0,0 +1,7 @@
+Git v2.43.4 Release Notes
+=========================
+
+This release merges up the fix that appears in v2.39.4, v2.40.2,
+v2.41.1 and v2.42.2 to address the security issues CVE-2024-32002,
+CVE-2024-32004, CVE-2024-32020, CVE-2024-32021 and CVE-2024-32465;
+see the release notes for these versions for details.
diff --git a/Documentation/RelNotes/2.43.5.txt b/Documentation/RelNotes/2.43.5.txt
new file mode 100644
index 0000000..236b234
--- /dev/null
+++ b/Documentation/RelNotes/2.43.5.txt
@@ -0,0 +1,26 @@
+Git v2.43.5 Release Notes
+=========================
+
+In preparing security fixes for four CVEs, we made overly aggressive
+"defense in depth" changes that broke legitimate use cases like 'git
+lfs' and 'git annex.'  This release is to revert these misguided, if
+well-intentioned, changes that were shipped in 2.43.4 and were not
+direct security fixes.
+
+Jeff King (5):
+      send-email: drop FakeTerm hack
+      send-email: avoid creating more than one Term::ReadLine object
+      ci: drop mention of BREW_INSTALL_PACKAGES variable
+      ci: avoid bare "gcc" for osx-gcc job
+      ci: stop installing "gcc-13" for osx-gcc
+
+Johannes Schindelin (6):
+      hook: plug a new memory leak
+      init: use the correct path of the templates directory again
+      Revert "core.hooksPath: add some protection while cloning"
+      tests: verify that `clone -c core.hooksPath=/dev/null` works again
+      clone: drop the protections where hooks aren't run
+      Revert "Add a helper function to compare file contents"
+
+Junio C Hamano (1):
+      Revert "fsck: warn about symlink pointing inside a gitdir"
diff --git a/Documentation/RelNotes/2.44.1.txt b/Documentation/RelNotes/2.44.1.txt
new file mode 100644
index 0000000..b5135c3
--- /dev/null
+++ b/Documentation/RelNotes/2.44.1.txt
@@ -0,0 +1,8 @@
+Git v2.44.1 Release Notes
+=========================
+
+This release merges up the fix that appears in v2.39.4, v2.40.2,
+v2.41.1, v2.42.2 and v2.43.4 to address the security issues
+CVE-2024-32002, CVE-2024-32004, CVE-2024-32020, CVE-2024-32021
+and CVE-2024-32465; see the release notes for these versions
+for details.
diff --git a/Documentation/RelNotes/2.44.2.txt b/Documentation/RelNotes/2.44.2.txt
new file mode 100644
index 0000000..76700f0
--- /dev/null
+++ b/Documentation/RelNotes/2.44.2.txt
@@ -0,0 +1,26 @@
+Git v2.44.2 Release Notes
+=========================
+
+In preparing security fixes for four CVEs, we made overly aggressive
+"defense in depth" changes that broke legitimate use cases like 'git
+lfs' and 'git annex.'  This release is to revert these misguided, if
+well-intentioned, changes that were shipped in 2.44.1 and were not
+direct security fixes.
+
+Jeff King (5):
+      send-email: drop FakeTerm hack
+      send-email: avoid creating more than one Term::ReadLine object
+      ci: drop mention of BREW_INSTALL_PACKAGES variable
+      ci: avoid bare "gcc" for osx-gcc job
+      ci: stop installing "gcc-13" for osx-gcc
+
+Johannes Schindelin (6):
+      hook: plug a new memory leak
+      init: use the correct path of the templates directory again
+      Revert "core.hooksPath: add some protection while cloning"
+      tests: verify that `clone -c core.hooksPath=/dev/null` works again
+      clone: drop the protections where hooks aren't run
+      Revert "Add a helper function to compare file contents"
+
+Junio C Hamano (1):
+      Revert "fsck: warn about symlink pointing inside a gitdir"
diff --git a/Documentation/RelNotes/2.45.1.txt b/Documentation/RelNotes/2.45.1.txt
new file mode 100644
index 0000000..3b0d60c
--- /dev/null
+++ b/Documentation/RelNotes/2.45.1.txt
@@ -0,0 +1,8 @@
+Git v2.45.1 Release Notes
+=========================
+
+This release merges up the fix that appears in v2.39.4,
+v2.40.2, v2.41.1, v2.42.2, v2.43.4 and v2.44.1 to address the
+security issues CVE-2024-32002, CVE-2024-32004, CVE-2024-32020,
+CVE-2024-32021 and CVE-2024-32465; see the release notes for
+these versions for details.
diff --git a/Documentation/RelNotes/2.45.2.txt b/Documentation/RelNotes/2.45.2.txt
new file mode 100644
index 0000000..13429e6
--- /dev/null
+++ b/Documentation/RelNotes/2.45.2.txt
@@ -0,0 +1,26 @@
+Git v2.45.2 Release Notes
+=========================
+
+In preparing security fixes for four CVEs, we made overly aggressive
+"defense in depth" changes that broke legitimate use cases like 'git
+lfs' and 'git annex.'  This release is to revert these misguided, if
+well-intentioned, changes that were shipped in 2.45.1 and were not
+direct security fixes.
+
+Jeff King (5):
+      send-email: drop FakeTerm hack
+      send-email: avoid creating more than one Term::ReadLine object
+      ci: drop mention of BREW_INSTALL_PACKAGES variable
+      ci: avoid bare "gcc" for osx-gcc job
+      ci: stop installing "gcc-13" for osx-gcc
+
+Johannes Schindelin (6):
+      hook: plug a new memory leak
+      init: use the correct path of the templates directory again
+      Revert "core.hooksPath: add some protection while cloning"
+      tests: verify that `clone -c core.hooksPath=/dev/null` works again
+      clone: drop the protections where hooks aren't run
+      Revert "Add a helper function to compare file contents"
+
+Junio C Hamano (1):
+      Revert "fsck: warn about symlink pointing inside a gitdir"
diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches
index c647c7e..625bdd0 100644
--- a/Documentation/SubmittingPatches
+++ b/Documentation/SubmittingPatches
@@ -415,10 +415,12 @@
 your code.  For this reason, each patch should be submitted
 "inline" in a separate message.
 
-Multiple related patches should be grouped into their own e-mail
-thread to help readers find all parts of the series.  To that end,
-send them as replies to either an additional "cover letter" message
-(see below), the first patch, or the respective preceding patch.
+All subsequent versions of a patch series and other related patches should be
+grouped into their own e-mail thread to help readers find all parts of the
+series.  To that end, send them as replies to either an additional "cover
+letter" message (see below), the first patch, or the respective preceding patch.
+Here is a link:MyFirstContribution.html#v2-git-send-email[step-by-step guide] on
+how to submit updated versions of a patch series.
 
 If your log message (including your name on the
 `Signed-off-by` trailer) is not writable in ASCII, make sure that
@@ -531,9 +533,9 @@
 Some parts of the system have dedicated maintainers with their own
 repositories.
 
-- `git-gui/` comes from git-gui project, maintained by Pratyush Yadav:
+- `git-gui/` comes from git-gui project, maintained by Johannes Sixt:
 
-	https://github.com/prati0100/git-gui.git
+        https://github.com/j6t/git-gui
 
 - `gitk-git/` comes from Paul Mackerras's gitk project:
 
@@ -548,6 +550,13 @@
 
 Patches to these parts should be based on their trees.
 
+- The "Git documentation translations" project, led by Jean-Noël
+  Avila, translates our documentation pages.  Their work products are
+  maintained separately from this project, not as part of our tree:
+
+	https://github.com/jnavila/git-manpages-l10n/
+
+
 [[patch-flow]]
 == An ideal patch flow
 
diff --git a/Documentation/git-for-each-repo.txt b/Documentation/git-for-each-repo.txt
index 94bd19d..abe3527 100644
--- a/Documentation/git-for-each-repo.txt
+++ b/Documentation/git-for-each-repo.txt
@@ -42,6 +42,15 @@
 as available. If `git for-each-repo` is run in a directory that is not a
 Git repository, then only the system and global config is used.
 
+--keep-going::
+	Continue with the remaining repositories if the command failed
+	on a repository. The exit code will still indicate that the
+	overall operation was not successful.
++
+Note that the exact exit code of the failing command is not passed
+through as the exit code of the `for-each-repo` command: If the command
+failed in any of the specified repositories, the overall exit code will
+be 1.
 
 SUBPROCESS BEHAVIOR
 -------------------
diff --git a/Documentation/git-gui.txt b/Documentation/git-gui.txt
index e8f3ccb..f5b02ef 100644
--- a/Documentation/git-gui.txt
+++ b/Documentation/git-gui.txt
@@ -114,7 +114,7 @@
 
 The official repository of the 'git gui' project can be found at:
 
-  https://github.com/prati0100/git-gui.git/
+  https://github.com/j6t/git-gui
 
 GIT
 ---
diff --git a/Documentation/git-upload-pack.txt b/Documentation/git-upload-pack.txt
index 7ad60bc..516d163 100644
--- a/Documentation/git-upload-pack.txt
+++ b/Documentation/git-upload-pack.txt
@@ -55,6 +55,37 @@
 	admins may need to configure some transports to allow this
 	variable to be passed. See the discussion in linkgit:git[1].
 
+`GIT_NO_LAZY_FETCH`::
+	When cloning or fetching from a partial repository (i.e., one
+	itself cloned with `--filter`), the server-side `upload-pack`
+	may need to fetch extra objects from its upstream in order to
+	complete the request. By default, `upload-pack` will refuse to
+	perform such a lazy fetch, because `git fetch` may run arbitrary
+	commands specified in configuration and hooks of the source
+	repository (and `upload-pack` tries to be safe to run even in
+	untrusted `.git` directories).
++
+This is implemented by having `upload-pack` internally set the
+`GIT_NO_LAZY_FETCH` variable to `1`. If you want to override it
+(because you are fetching from a partial clone, and you are sure
+you trust it), you can explicitly set `GIT_NO_LAZY_FETCH` to
+`0`.
+
+SECURITY
+--------
+
+Most Git commands should not be run in an untrusted `.git` directory
+(see the section `SECURITY` in linkgit:git[1]). `upload-pack` tries to
+avoid any dangerous configuration options or hooks from the repository
+it's serving, making it safe to clone an untrusted directory and run
+commands on the resulting clone.
+
+For an extra level of safety, you may be able to run `upload-pack` as an
+alternate user. The details will be platform dependent, but on many
+systems you can run:
+
+    git clone --no-local --upload-pack='sudo -u nobody git-upload-pack' ...
+
 SEE ALSO
 --------
 linkgit:gitnamespaces[7]
diff --git a/Documentation/git.txt b/Documentation/git.txt
index 7a1b112..024a01d 100644
--- a/Documentation/git.txt
+++ b/Documentation/git.txt
@@ -1067,6 +1067,37 @@
 for a given pathname.  These stages are used to hold the various
 unmerged version of a file when a merge is in progress.
 
+SECURITY
+--------
+
+Some configuration options and hook files may cause Git to run arbitrary
+shell commands. Because configuration and hooks are not copied using
+`git clone`, it is generally safe to clone remote repositories with
+untrusted content, inspect them with `git log`, and so on.
+
+However, it is not safe to run Git commands in a `.git` directory (or
+the working tree that surrounds it) when that `.git` directory itself
+comes from an untrusted source. The commands in its config and hooks
+are executed in the usual way.
+
+By default, Git will refuse to run when the repository is owned by
+someone other than the user running the command. See the entry for
+`safe.directory` in linkgit:git-config[1]. While this can help protect
+you in a multi-user environment, note that you can also acquire
+untrusted repositories that are owned by you (for example, if you
+extract a zip file or tarball from an untrusted source). In such cases,
+you'd need to "sanitize" the untrusted repository first.
+
+If you have an untrusted `.git` directory, you should first clone it
+with `git clone --no-local` to obtain a clean copy. Git does restrict
+the set of options and hooks that will be run by `upload-pack`, which
+handles the server side of a clone or fetch, but beware that the
+surface area for attack against `upload-pack` is large, so this does
+carry some risk. The safest thing is to serve the repository as an
+unprivileged user (either via linkgit:git-daemon[1], ssh, or using
+other tools to change user ids). See the discussion in the `SECURITY`
+section of linkgit:git-upload-pack[1].
+
 FURTHER DOCUMENTATION
 ---------------------
 
diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN
index 433cf41..eb53b0e 100755
--- a/GIT-VERSION-GEN
+++ b/GIT-VERSION-GEN
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 GVF=GIT-VERSION-FILE
-DEF_VER=v2.45.0
+DEF_VER=v2.45.2
 
 LF='
 '
diff --git a/RelNotes b/RelNotes
index ae70277..2079120 120000
--- a/RelNotes
+++ b/RelNotes
@@ -1 +1 @@
-Documentation/RelNotes/2.45.0.txt
\ No newline at end of file
+Documentation/RelNotes/2.45.2.txt
\ No newline at end of file
diff --git a/attr.c b/attr.c
index 679e422..33473bd 100644
--- a/attr.c
+++ b/attr.c
@@ -765,8 +765,8 @@ static struct attr_stack *read_attr_from_file(const char *path, unsigned flags)
 	return res;
 }
 
-static struct attr_stack *read_attr_from_buf(char *buf, const char *path,
-					     unsigned flags)
+static struct attr_stack *read_attr_from_buf(char *buf, size_t length,
+					     const char *path, unsigned flags)
 {
 	struct attr_stack *res;
 	char *sp;
@@ -774,6 +774,11 @@ static struct attr_stack *read_attr_from_buf(char *buf, const char *path,
 
 	if (!buf)
 		return NULL;
+	if (length >= ATTR_MAX_FILE_SIZE) {
+		warning(_("ignoring overly large gitattributes blob '%s'"), path);
+		free(buf);
+		return NULL;
+	}
 
 	CALLOC_ARRAY(res, 1);
 	for (sp = buf; *sp;) {
@@ -813,7 +818,7 @@ static struct attr_stack *read_attr_from_blob(struct index_state *istate,
 		return NULL;
 	}
 
-	return read_attr_from_buf(buf, path, flags);
+	return read_attr_from_buf(buf, sz, path, flags);
 }
 
 static struct attr_stack *read_attr_from_index(struct index_state *istate,
@@ -860,13 +865,7 @@ static struct attr_stack *read_attr_from_index(struct index_state *istate,
 		stack = read_attr_from_blob(istate, &istate->cache[sparse_dir_pos]->oid, relative_path, flags);
 	} else {
 		buf = read_blob_data_from_index(istate, path, &size);
-		if (!buf)
-			return NULL;
-		if (size >= ATTR_MAX_FILE_SIZE) {
-			warning(_("ignoring overly large gitattributes blob '%s'"), path);
-			return NULL;
-		}
-		stack = read_attr_from_buf(buf, path, flags);
+		stack = read_attr_from_buf(buf, size, path, flags);
 	}
 	return stack;
 }
@@ -1223,13 +1222,6 @@ static void compute_default_attr_source(struct object_id *attr_source)
 		ignore_bad_attr_tree = 1;
 	}
 
-	if (!default_attr_source_tree_object_name &&
-	    startup_info->have_repository &&
-	    is_bare_repository()) {
-		default_attr_source_tree_object_name = "HEAD";
-		ignore_bad_attr_tree = 1;
-	}
-
 	if (!default_attr_source_tree_object_name || !is_null_oid(attr_source))
 		return;
 
diff --git a/builtin/clone.c b/builtin/clone.c
index 74ec145..5fbe39f 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -329,7 +329,20 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
 	int src_len, dest_len;
 	struct dir_iterator *iter;
 	int iter_status;
-	struct strbuf realpath = STRBUF_INIT;
+
+	/*
+	 * Refuse copying directories by default which aren't owned by us. The
+	 * code that performs either the copying or hardlinking is not prepared
+	 * to handle various edge cases where an adversary may for example
+	 * racily swap out files for symlinks. This can cause us to
+	 * inadvertently use the wrong source file.
+	 *
+	 * Furthermore, even if we were prepared to handle such races safely,
+	 * creating hardlinks across user boundaries is an inherently unsafe
+	 * operation as the hardlinked files can be rewritten at will by the
+	 * potentially-untrusted user. We thus refuse to do so by default.
+	 */
+	die_upon_dubious_ownership(NULL, NULL, src_repo);
 
 	mkdir_if_missing(dest->buf, 0777);
 
@@ -377,9 +390,27 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
 		if (unlink(dest->buf) && errno != ENOENT)
 			die_errno(_("failed to unlink '%s'"), dest->buf);
 		if (!option_no_hardlinks) {
-			strbuf_realpath(&realpath, src->buf, 1);
-			if (!link(realpath.buf, dest->buf))
+			if (!link(src->buf, dest->buf)) {
+				struct stat st;
+
+				/*
+				 * Sanity-check whether the created hardlink
+				 * actually links to the expected file now. This
+				 * catches time-of-check-time-of-use bugs in
+				 * case the source file was meanwhile swapped.
+				 */
+				if (lstat(dest->buf, &st))
+					die(_("hardlink cannot be checked at '%s'"), dest->buf);
+				if (st.st_mode != iter->st.st_mode ||
+				    st.st_ino != iter->st.st_ino ||
+				    st.st_dev != iter->st.st_dev ||
+				    st.st_size != iter->st.st_size ||
+				    st.st_uid != iter->st.st_uid ||
+				    st.st_gid != iter->st.st_gid)
+					die(_("hardlink different from source at '%s'"), dest->buf);
+
 				continue;
+			}
 			if (option_local > 0)
 				die_errno(_("failed to create link '%s'"), dest->buf);
 			option_no_hardlinks = 1;
@@ -392,8 +423,6 @@ static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest,
 		strbuf_setlen(src, src_len);
 		die(_("failed to iterate over '%s'"), src->buf);
 	}
-
-	strbuf_release(&realpath);
 }
 
 static void clone_local(const char *src_repo, const char *dest_repo)
@@ -1507,6 +1536,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
 	free(dir);
 	free(path);
 	free(repo_to_free);
+	UNLEAK(repo);
 	junk_mode = JUNK_LEAVE_ALL;
 
 	transport_ls_refs_options_release(&transport_ls_refs_options);
diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c
index 28186b3..c4fa41f 100644
--- a/builtin/for-each-repo.c
+++ b/builtin/for-each-repo.c
@@ -32,6 +32,7 @@ static int run_command_on_repo(const char *path, int argc, const char ** argv)
 int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 {
 	static const char *config_key = NULL;
+	int keep_going = 0;
 	int i, result = 0;
 	const struct string_list *values;
 	int err;
@@ -39,6 +40,8 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	const struct option options[] = {
 		OPT_STRING(0, "config", &config_key, N_("config"),
 			   N_("config key storing a list of repository paths")),
+		OPT_BOOL(0, "keep-going", &keep_going,
+			 N_("keep going even if command fails in a repository")),
 		OPT_END()
 	};
 
@@ -55,8 +58,14 @@ int cmd_for_each_repo(int argc, const char **argv, const char *prefix)
 	else if (err)
 		return 0;
 
-	for (i = 0; !result && i < values->nr; i++)
-		result = run_command_on_repo(values->items[i].string, argc, argv);
+	for (i = 0; i < values->nr; i++) {
+		int ret = run_command_on_repo(values->items[i].string, argc, argv);
+		if (ret) {
+			if (!keep_going)
+					return ret;
+			result = 1;
+		}
+	}
 
 	return result;
 }
diff --git a/builtin/gc.c b/builtin/gc.c
index d187cec..d3b5ca9 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -1870,6 +1870,7 @@ static int launchctl_schedule_plist(const char *exec_path, enum schedule_priorit
 		   "<string>%s/git</string>\n"
 		   "<string>--exec-path=%s</string>\n"
 		   "<string>for-each-repo</string>\n"
+		   "<string>--keep-going</string>\n"
 		   "<string>--config=maintenance.repo</string>\n"
 		   "<string>maintenance</string>\n"
 		   "<string>run</string>\n"
@@ -2112,7 +2113,7 @@ static int schtasks_schedule_task(const char *exec_path, enum schedule_priority
 	      "<Actions Context=\"Author\">\n"
 	      "<Exec>\n"
 	      "<Command>\"%s\\headless-git.exe\"</Command>\n"
-	      "<Arguments>--exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n"
+	      "<Arguments>--exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n"
 	      "</Exec>\n"
 	      "</Actions>\n"
 	      "</Task>\n";
@@ -2257,7 +2258,7 @@ static int crontab_update_schedule(int run_maintenance, int fd)
 			"# replaced in the future by a Git command.\n\n");
 
 		strbuf_addf(&line_format,
-			    "%%d %%s * * %%s \"%s/git\" --exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%s\n",
+			    "%%d %%s * * %%s \"%s/git\" --exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%s\n",
 			    exec_path, exec_path);
 		fprintf(cron_in, line_format.buf, minute, "1-23", "*", "hourly");
 		fprintf(cron_in, line_format.buf, minute, "0", "1-6", "daily");
@@ -2458,7 +2459,7 @@ static int systemd_timer_write_service_template(const char *exec_path)
 	       "\n"
 	       "[Service]\n"
 	       "Type=oneshot\n"
-	       "ExecStart=\"%s/git\" --exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\n"
+	       "ExecStart=\"%s/git\" --exec-path=\"%s\" for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=%%i\n"
 	       "LockPersonality=yes\n"
 	       "MemoryDenyWriteExecute=yes\n"
 	       "NoNewPrivileges=yes\n"
diff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c
index a72aebe..8360932 100644
--- a/builtin/multi-pack-index.c
+++ b/builtin/multi-pack-index.c
@@ -8,6 +8,7 @@
 #include "strbuf.h"
 #include "trace2.h"
 #include "object-store-ll.h"
+#include "replace-object.h"
 
 #define BUILTIN_MIDX_WRITE_USAGE \
 	N_("git multi-pack-index [<options>] write [--preferred-pack=<pack>]" \
@@ -273,6 +274,8 @@ int cmd_multi_pack_index(int argc, const char **argv,
 	};
 	struct option *options = parse_options_concat(builtin_multi_pack_index_options, common_opts);
 
+	disable_replace_refs();
+
 	git_config(git_default_config, NULL);
 
 	if (the_repository &&
diff --git a/builtin/stash.c b/builtin/stash.c
index 062be1f..7751bca 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1205,8 +1205,8 @@ static int stash_staged(struct stash_info *info, struct strbuf *out_patch,
 	}
 
 	cp_diff_tree.git_cmd = 1;
-	strvec_pushl(&cp_diff_tree.args, "diff-tree", "-p", "-U1", "HEAD",
-		     oid_to_hex(&info->w_tree), "--", NULL);
+	strvec_pushl(&cp_diff_tree.args, "diff-tree", "-p", "--binary",
+		     "-U1", "HEAD", oid_to_hex(&info->w_tree), "--", NULL);
 	if (pipe_command(&cp_diff_tree, NULL, 0, out_patch, 0, NULL, 0)) {
 		ret = -1;
 		goto done;
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index e4e18ad..5a71b1e 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -303,6 +303,9 @@ static void runcommand_in_submodule_cb(const struct cache_entry *list_item,
 	struct child_process cp = CHILD_PROCESS_INIT;
 	char *displaypath;
 
+	if (validate_submodule_path(path) < 0)
+		exit(128);
+
 	displaypath = get_submodule_displaypath(path, info->prefix,
 						info->super_prefix);
 
@@ -634,6 +637,9 @@ static void status_submodule(const char *path, const struct object_id *ce_oid,
 		.free_removed_argv_elements = 1,
 	};
 
+	if (validate_submodule_path(path) < 0)
+		exit(128);
+
 	if (!submodule_from_path(the_repository, null_oid(), path))
 		die(_("no submodule mapping found in .gitmodules for path '%s'"),
 		      path);
@@ -1238,6 +1244,9 @@ static void sync_submodule(const char *path, const char *prefix,
 	if (!is_submodule_active(the_repository, path))
 		return;
 
+	if (validate_submodule_path(path) < 0)
+		exit(128);
+
 	sub = submodule_from_path(the_repository, null_oid(), path);
 
 	if (sub && sub->url) {
@@ -1381,6 +1390,9 @@ static void deinit_submodule(const char *path, const char *prefix,
 	struct strbuf sb_config = STRBUF_INIT;
 	char *sub_git_dir = xstrfmt("%s/.git", path);
 
+	if (validate_submodule_path(path) < 0)
+		exit(128);
+
 	sub = submodule_from_path(the_repository, null_oid(), path);
 
 	if (!sub || !sub->name)
@@ -1662,16 +1674,42 @@ static char *clone_submodule_sm_gitdir(const char *name)
 	return sm_gitdir;
 }
 
+static int dir_contains_only_dotgit(const char *path)
+{
+	DIR *dir = opendir(path);
+	struct dirent *e;
+	int ret = 1;
+
+	if (!dir)
+		return 0;
+
+	e = readdir_skip_dot_and_dotdot(dir);
+	if (!e)
+		ret = 0;
+	else if (strcmp(DEFAULT_GIT_DIR_ENVIRONMENT, e->d_name) ||
+		 (e = readdir_skip_dot_and_dotdot(dir))) {
+		error("unexpected item '%s' in '%s'", e->d_name, path);
+		ret = 0;
+	}
+
+	closedir(dir);
+	return ret;
+}
+
 static int clone_submodule(const struct module_clone_data *clone_data,
 			   struct string_list *reference)
 {
 	char *p;
 	char *sm_gitdir = clone_submodule_sm_gitdir(clone_data->name);
 	char *sm_alternate = NULL, *error_strategy = NULL;
+	struct stat st;
 	struct child_process cp = CHILD_PROCESS_INIT;
 	const char *clone_data_path = clone_data->path;
 	char *to_free = NULL;
 
+	if (validate_submodule_path(clone_data_path) < 0)
+		exit(128);
+
 	if (!is_absolute_path(clone_data->path))
 		clone_data_path = to_free = xstrfmt("%s/%s", get_git_work_tree(),
 						    clone_data->path);
@@ -1681,6 +1719,10 @@ static int clone_submodule(const struct module_clone_data *clone_data,
 		      "git dir"), sm_gitdir);
 
 	if (!file_exists(sm_gitdir)) {
+		if (clone_data->require_init && !stat(clone_data_path, &st) &&
+		    !is_empty_dir(clone_data_path))
+			die(_("directory not empty: '%s'"), clone_data_path);
+
 		if (safe_create_leading_directories_const(sm_gitdir) < 0)
 			die(_("could not create directory '%s'"), sm_gitdir);
 
@@ -1725,10 +1767,18 @@ static int clone_submodule(const struct module_clone_data *clone_data,
 		if(run_command(&cp))
 			die(_("clone of '%s' into submodule path '%s' failed"),
 			    clone_data->url, clone_data_path);
+
+		if (clone_data->require_init && !stat(clone_data_path, &st) &&
+		    !dir_contains_only_dotgit(clone_data_path)) {
+			char *dot_git = xstrfmt("%s/.git", clone_data_path);
+			unlink(dot_git);
+			free(dot_git);
+			die(_("directory not empty: '%s'"), clone_data_path);
+		}
 	} else {
 		char *path;
 
-		if (clone_data->require_init && !access(clone_data_path, X_OK) &&
+		if (clone_data->require_init && !stat(clone_data_path, &st) &&
 		    !is_empty_dir(clone_data_path))
 			die(_("directory not empty: '%s'"), clone_data_path);
 		if (safe_create_leading_directories_const(clone_data_path) < 0)
@@ -1738,6 +1788,23 @@ static int clone_submodule(const struct module_clone_data *clone_data,
 		free(path);
 	}
 
+	/*
+	 * We already performed this check at the beginning of this function,
+	 * before cloning the objects. This tries to detect racy behavior e.g.
+	 * in parallel clones, where another process could easily have made the
+	 * gitdir nested _after_ it was created.
+	 *
+	 * To prevent further harm coming from this unintentionally-nested
+	 * gitdir, let's disable it by deleting the `HEAD` file.
+	 */
+	if (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0) {
+		char *head = xstrfmt("%s/HEAD", sm_gitdir);
+		unlink(head);
+		free(head);
+		die(_("refusing to create/use '%s' in another submodule's "
+		      "git dir"), sm_gitdir);
+	}
+
 	connect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0);
 
 	p = git_pathdup_submodule(clone_data_path, "config");
@@ -2517,6 +2584,9 @@ static int update_submodule(struct update_data *update_data)
 {
 	int ret;
 
+	if (validate_submodule_path(update_data->sm_path) < 0)
+		return -1;
+
 	ret = determine_submodule_update_strategy(the_repository,
 						  update_data->just_cloned,
 						  update_data->sm_path,
@@ -2624,12 +2694,21 @@ static int update_submodules(struct update_data *update_data)
 
 	for (i = 0; i < suc.update_clone_nr; i++) {
 		struct update_clone_data ucd = suc.update_clone[i];
-		int code;
+		int code = 128;
 
 		oidcpy(&update_data->oid, &ucd.oid);
 		update_data->just_cloned = ucd.just_cloned;
 		update_data->sm_path = ucd.sub->path;
 
+		/*
+		 * Verify that the submodule path does not contain any
+		 * symlinks; if it does, it might have been tampered with.
+		 * TODO: allow exempting it via
+		 * `safe.submodule.path` or something
+		 */
+		if (validate_submodule_path(update_data->sm_path) < 0)
+			goto fail;
+
 		code = ensure_core_worktree(update_data->sm_path);
 		if (code)
 			goto fail;
@@ -3356,6 +3435,9 @@ static int module_add(int argc, const char **argv, const char *prefix)
 	normalize_path_copy(add_data.sm_path, add_data.sm_path);
 	strip_dir_trailing_slashes(add_data.sm_path);
 
+	if (validate_submodule_path(add_data.sm_path) < 0)
+		exit(128);
+
 	die_on_index_match(add_data.sm_path, force);
 	die_on_repo_without_commits(add_data.sm_path);
 
diff --git a/builtin/upload-pack.c b/builtin/upload-pack.c
index 15afb97..46d9327 100644
--- a/builtin/upload-pack.c
+++ b/builtin/upload-pack.c
@@ -9,6 +9,7 @@
 #include "upload-pack.h"
 #include "serve.h"
 #include "commit.h"
+#include "environment.h"
 
 static const char * const upload_pack_usage[] = {
 	N_("git-upload-pack [--[no-]strict] [--timeout=<n>] [--stateless-rpc]\n"
@@ -39,6 +40,7 @@ int cmd_upload_pack(int argc, const char **argv, const char *prefix)
 	packet_trace_identity("upload-pack");
 	disable_replace_refs();
 	save_commit_buffer = 0;
+	xsetenv(NO_LAZY_FETCH_ENVIRONMENT, "1", 0);
 
 	argc = parse_options(argc, argv, prefix, options, upload_pack_usage, 0);
 
diff --git a/ci/install-dependencies.sh b/ci/install-dependencies.sh
index b4e22de..fa0c00c 100755
--- a/ci/install-dependencies.sh
+++ b/ci/install-dependencies.sh
@@ -34,8 +34,6 @@
 	export HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1
 	# Uncomment this if you want to run perf tests:
 	# brew install gnu-time
-	test -z "$BREW_INSTALL_PACKAGES" ||
-	brew install $BREW_INSTALL_PACKAGES
 	brew link --force gettext
 
 	mkdir -p "$P4_PATH"
diff --git a/ci/lib.sh b/ci/lib.sh
index 0a73fc7..3856c00 100755
--- a/ci/lib.sh
+++ b/ci/lib.sh
@@ -325,9 +325,13 @@
 		break
 	fi
 
-	PYTHON_PACKAGE=python2
-	if test "$jobname" = linux-gcc
+	# Python 2 is end of life, and Ubuntu 23.04 and newer don't actually
+	# have it anymore. We thus only test with Python 2 on older LTS
+	# releases.
+	if "$distro" = "ubuntu-20.04"
 	then
+		PYTHON_PACKAGE=python2
+	else
 		PYTHON_PACKAGE=python3
 	fi
 	MAKEFLAGS="$MAKEFLAGS PYTHON_PATH=/usr/bin/$PYTHON_PACKAGE"
diff --git a/compat/mingw.c b/compat/mingw.c
index 4876344..6b06ea5 100644
--- a/compat/mingw.c
+++ b/compat/mingw.c
@@ -3159,6 +3159,7 @@ int uname(struct utsname *buf)
 	return 0;
 }
 
+#ifndef NO_UNIX_SOCKETS
 int mingw_have_unix_sockets(void)
 {
 	SC_HANDLE scm, srvc;
@@ -3177,3 +3178,4 @@ int mingw_have_unix_sockets(void)
 	}
 	return ret;
 }
+#endif
diff --git a/compat/regex/regcomp.c b/compat/regex/regcomp.c
index d1bc09e..2bc0f11 100644
--- a/compat/regex/regcomp.c
+++ b/compat/regex/regcomp.c
@@ -868,7 +868,7 @@ init_dfa (re_dfa_t *dfa, size_t pat_len)
     if (table_size > pat_len)
       break;
 
-  dfa->state_table = calloc (sizeof (struct re_state_table_entry), table_size);
+  dfa->state_table = calloc (table_size, sizeof (struct re_state_table_entry));
   dfa->state_hash_mask = table_size - 1;
 
   dfa->mb_cur_max = MB_CUR_MAX;
@@ -936,7 +936,7 @@ init_dfa (re_dfa_t *dfa, size_t pat_len)
 	{
 	  int i, j, ch;
 
-	  dfa->sb_char = (re_bitset_ptr_t) calloc (sizeof (bitset_t), 1);
+	  dfa->sb_char = (re_bitset_ptr_t) calloc (1, sizeof (bitset_t));
 	  if (BE (dfa->sb_char == NULL, 0))
 	    return REG_ESPACE;
 
@@ -3079,9 +3079,9 @@ parse_bracket_exp (re_string_t *regexp, re_dfa_t *dfa, re_token_t *token,
 						   _NL_COLLATE_SYMB_EXTRAMB);
     }
 #endif
-  sbcset = (re_bitset_ptr_t) calloc (sizeof (bitset_t), 1);
+  sbcset = (re_bitset_ptr_t) calloc (1, sizeof (bitset_t));
 #ifdef RE_ENABLE_I18N
-  mbcset = (re_charset_t *) calloc (sizeof (re_charset_t), 1);
+  mbcset = (re_charset_t *) calloc (1, sizeof (re_charset_t));
 #endif /* RE_ENABLE_I18N */
 #ifdef RE_ENABLE_I18N
   if (BE (sbcset == NULL || mbcset == NULL, 0))
@@ -3626,9 +3626,9 @@ build_charclass_op (re_dfa_t *dfa, RE_TRANSLATE_TYPE trans,
   re_token_t br_token;
   bin_tree_t *tree;
 
-  sbcset = (re_bitset_ptr_t) calloc (sizeof (bitset_t), 1);
+  sbcset = (re_bitset_ptr_t) calloc (1, sizeof (bitset_t));
 #ifdef RE_ENABLE_I18N
-  mbcset = (re_charset_t *) calloc (sizeof (re_charset_t), 1);
+  mbcset = (re_charset_t *) calloc (1, sizeof (re_charset_t));
 #endif /* RE_ENABLE_I18N */
 
 #ifdef RE_ENABLE_I18N
diff --git a/compat/regex/regex_internal.c b/compat/regex/regex_internal.c
index ec51cf3..ec5cc5d 100644
--- a/compat/regex/regex_internal.c
+++ b/compat/regex/regex_internal.c
@@ -1628,7 +1628,7 @@ create_ci_newstate (const re_dfa_t *dfa, const re_node_set *nodes,
   reg_errcode_t err;
   re_dfastate_t *newstate;
 
-  newstate = (re_dfastate_t *) calloc (sizeof (re_dfastate_t), 1);
+  newstate = (re_dfastate_t *) calloc (1, sizeof (re_dfastate_t));
   if (BE (newstate == NULL, 0))
     return NULL;
   err = re_node_set_init_copy (&newstate->nodes, nodes);
@@ -1678,7 +1678,7 @@ create_cd_newstate (const re_dfa_t *dfa, const re_node_set *nodes,
   reg_errcode_t err;
   re_dfastate_t *newstate;
 
-  newstate = (re_dfastate_t *) calloc (sizeof (re_dfastate_t), 1);
+  newstate = (re_dfastate_t *) calloc (1, sizeof (re_dfastate_t));
   if (BE (newstate == NULL, 0))
     return NULL;
   err = re_node_set_init_copy (&newstate->nodes, nodes);
diff --git a/compat/regex/regexec.c b/compat/regex/regexec.c
index 49358ae..e92be57 100644
--- a/compat/regex/regexec.c
+++ b/compat/regex/regexec.c
@@ -2796,8 +2796,8 @@ get_subexp (re_match_context_t *mctx, int bkref_node, int bkref_str_idx)
 	    continue; /* No.  */
 	  if (sub_top->path == NULL)
 	    {
-	      sub_top->path = calloc (sizeof (state_array_t),
-				      sl_str - sub_top->str_idx + 1);
+	      sub_top->path = calloc (sl_str - sub_top->str_idx + 1,
+				      sizeof (state_array_t));
 	      if (sub_top->path == NULL)
 		return REG_ESPACE;
 	    }
@@ -3361,7 +3361,7 @@ build_trtable (const re_dfa_t *dfa, re_dfastate_t *state)
       if (ndests == 0)
 	{
 	  state->trtable = (re_dfastate_t **)
-	    calloc (sizeof (re_dfastate_t *), SBC_MAX);
+	    calloc (SBC_MAX, sizeof (re_dfastate_t *));
 	  return 1;
 	}
       return 0;
@@ -3457,7 +3457,7 @@ build_trtable (const re_dfa_t *dfa, re_dfastate_t *state)
 	 discern by looking at the character code: allocate a
 	 256-entry transition table.  */
       trtable = state->trtable =
-	(re_dfastate_t **) calloc (sizeof (re_dfastate_t *), SBC_MAX);
+	(re_dfastate_t **) calloc (SBC_MAX, sizeof (re_dfastate_t *));
       if (BE (trtable == NULL, 0))
 	goto out_free;
 
@@ -3488,7 +3488,7 @@ build_trtable (const re_dfa_t *dfa, re_dfastate_t *state)
 	 transition tables, one starting at trtable[0] and one
 	 starting at trtable[SBC_MAX].  */
       trtable = state->word_trtable =
-	(re_dfastate_t **) calloc (sizeof (re_dfastate_t *), 2 * SBC_MAX);
+	(re_dfastate_t **) calloc (2 * SBC_MAX, sizeof (re_dfastate_t *));
       if (BE (trtable == NULL, 0))
 	goto out_free;
 
diff --git a/contrib/completion/git-completion.zsh b/contrib/completion/git-completion.zsh
index cac6f61..f5877bd 100644
--- a/contrib/completion/git-completion.zsh
+++ b/contrib/completion/git-completion.zsh
@@ -272,6 +272,7 @@
 {
 	local _ret=1
 	local cur cword prev
+	local __git_repo_path
 
 	cur=${words[CURRENT]}
 	prev=${words[CURRENT-1]}
diff --git a/copy.c b/copy.c
index 23d84c6..d9d2092 100644
--- a/copy.c
+++ b/copy.c
@@ -1,6 +1,9 @@
 #include "git-compat-util.h"
 #include "copy.h"
 #include "path.h"
+#include "gettext.h"
+#include "strbuf.h"
+#include "abspath.h"
 
 int copy_fd(int ifd, int ofd)
 {
diff --git a/diff-lib.c b/diff-lib.c
index 683f11e..12b1541 100644
--- a/diff-lib.c
+++ b/diff-lib.c
@@ -660,7 +660,6 @@ int do_diff_cache(const struct object_id *tree_oid, struct diff_options *opt)
 
 	repo_init_revisions(opt->repo, &revs, NULL);
 	copy_pathspec(&revs.prune_data, &opt->pathspec);
-	diff_setup_done(&revs.diffopt);
 	revs.diffopt = *opt;
 
 	if (diff_cache(&revs, tree_oid, NULL, 1))
diff --git a/dir.c b/dir.c
index 20ebe4c..2d83f33 100644
--- a/dir.c
+++ b/dir.c
@@ -100,6 +100,18 @@ int fspathncmp(const char *a, const char *b, size_t count)
 	return ignore_case ? strncasecmp(a, b, count) : strncmp(a, b, count);
 }
 
+int paths_collide(const char *a, const char *b)
+{
+	size_t len_a = strlen(a), len_b = strlen(b);
+
+	if (len_a == len_b)
+		return fspatheq(a, b);
+
+	if (len_a < len_b)
+		return is_dir_sep(b[len_a]) && !fspathncmp(a, b, len_a);
+	return is_dir_sep(a[len_b]) && !fspathncmp(a, b, len_b);
+}
+
 unsigned int fspathhash(const char *str)
 {
 	return ignore_case ? strihash(str) : strhash(str);
diff --git a/dir.h b/dir.h
index 45a7b9e..b9e8e96 100644
--- a/dir.h
+++ b/dir.h
@@ -549,6 +549,13 @@ int fspathncmp(const char *a, const char *b, size_t count);
 unsigned int fspathhash(const char *str);
 
 /*
+ * Reports whether paths collide. This may be because the paths differ only in
+ * case on a case-sensitive filesystem, or that one path refers to a symlink
+ * that collides with one of the parent directories of the other.
+ */
+int paths_collide(const char *a, const char *b);
+
+/*
  * The prefix part of pattern must not contains wildcards.
  */
 struct pathspec_item;
diff --git a/entry.c b/entry.c
index f918a3a..b8c257f 100644
--- a/entry.c
+++ b/entry.c
@@ -460,7 +460,7 @@ static void mark_colliding_entries(const struct checkout *state,
 			continue;
 
 		if ((trust_ino && !match_stat_data(&dup->ce_stat_data, st)) ||
-		    (!trust_ino && !fspathcmp(ce->name, dup->name))) {
+		    paths_collide(ce->name, dup->name)) {
 			dup->ce_flags |= CE_MATCHED;
 			break;
 		}
@@ -547,6 +547,20 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca,
 			/* If it is a gitlink, leave it alone! */
 			if (S_ISGITLINK(ce->ce_mode))
 				return 0;
+			/*
+			 * We must avoid replacing submodules' leading
+			 * directories with symbolic links, lest recursive
+			 * clones can write into arbitrary locations.
+			 *
+			 * Technically, this logic is not limited
+			 * to recursive clones, or for that matter to
+			 * submodules' paths colliding with symbolic links'
+			 * paths. Yet it strikes a balance in favor of
+			 * simplicity, and if paths are colliding, we might
+			 * just as well keep the directories during a clone.
+			 */
+			if (state->clone && S_ISLNK(ce->ce_mode))
+				return 0;
 			remove_subtree(&path);
 		} else if (unlink(path.buf))
 			return error_errno("unable to unlink old '%s'", path.buf);
diff --git a/hook.c b/hook.c
index f6306d7..7e90787 100644
--- a/hook.c
+++ b/hook.c
@@ -7,25 +7,31 @@
 #include "run-command.h"
 #include "config.h"
 #include "strbuf.h"
+#include "environment.h"
+#include "setup.h"
 
 const char *find_hook(const char *name)
 {
 	static struct strbuf path = STRBUF_INIT;
 
+	int found_hook;
+
 	strbuf_reset(&path);
 	strbuf_git_path(&path, "hooks/%s", name);
-	if (access(path.buf, X_OK) < 0) {
+	found_hook = access(path.buf, X_OK) >= 0;
+#ifdef STRIP_EXTENSION
+	if (!found_hook) {
 		int err = errno;
 
-#ifdef STRIP_EXTENSION
 		strbuf_addstr(&path, STRIP_EXTENSION);
-		if (access(path.buf, X_OK) >= 0)
-			return path.buf;
-		if (errno == EACCES)
-			err = errno;
+		found_hook = access(path.buf, X_OK) >= 0;
+		if (!found_hook)
+			errno = err;
+	}
 #endif
 
-		if (err == EACCES && advice_enabled(ADVICE_IGNORED_HOOK)) {
+	if (!found_hook) {
+		if (errno == EACCES && advice_enabled(ADVICE_IGNORED_HOOK)) {
 			static struct string_list advise_given = STRING_LIST_INIT_DUP;
 
 			if (!string_list_lookup(&advise_given, name)) {
diff --git a/mergetools/vimdiff b/mergetools/vimdiff
index 97e3763..734d15a 100644
--- a/mergetools/vimdiff
+++ b/mergetools/vimdiff
@@ -72,7 +72,6 @@
 	nested=0
 	nested_min=100
 
-
 	# Step 1:
 	#
 	# Increase/decrease "start"/"end" indices respectively to get rid of
@@ -87,7 +86,7 @@
 	IFS=#
 	for c in $(echo "$LAYOUT" | sed 's:.:&#:g')
 	do
-		if test "$c" = " "
+		if test -z "$c" || test "$c" = " "
 		then
 			continue
 		fi
diff --git a/path.c b/path.c
index 67229ed..d838d4b 100644
--- a/path.c
+++ b/path.c
@@ -829,6 +829,7 @@ const char *enter_repo(const char *path, int strict)
 		if (!suffix[i])
 			return NULL;
 		gitfile = read_gitfile(used_path.buf);
+		die_upon_dubious_ownership(gitfile, NULL, used_path.buf);
 		if (gitfile) {
 			strbuf_reset(&used_path);
 			strbuf_addstr(&used_path, gitfile);
@@ -839,6 +840,7 @@ const char *enter_repo(const char *path, int strict)
 	}
 	else {
 		const char *gitfile = read_gitfile(path);
+		die_upon_dubious_ownership(gitfile, NULL, path);
 		if (gitfile)
 			path = gitfile;
 		if (chdir(path))
diff --git a/promisor-remote.c b/promisor-remote.c
index ac3aa1e..b414922 100644
--- a/promisor-remote.c
+++ b/promisor-remote.c
@@ -8,6 +8,7 @@
 #include "transport.h"
 #include "strvec.h"
 #include "packfile.h"
+#include "environment.h"
 
 struct promisor_remote_config {
 	struct promisor_remote *promisors;
@@ -23,6 +24,15 @@ static int fetch_objects(struct repository *repo,
 	int i;
 	FILE *child_in;
 
+	if (git_env_bool(NO_LAZY_FETCH_ENVIRONMENT, 0)) {
+		static int warning_shown;
+		if (!warning_shown) {
+			warning_shown = 1;
+			warning(_("lazy fetching disabled; some objects may not be available"));
+		}
+		return -1;
+	}
+
 	child.git_cmd = 1;
 	child.in = -1;
 	if (repo != the_repository)
diff --git a/read-cache.c b/read-cache.c
index e1723ad..a6db25a 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -1116,19 +1116,32 @@ static int has_dir_name(struct index_state *istate,
 			istate->cache[istate->cache_nr - 1]->name,
 			&len_eq_last);
 		if (cmp_last > 0) {
-			if (len_eq_last == 0) {
+			if (name[len_eq_last] != '/') {
 				/*
 				 * The entry sorts AFTER the last one in the
-				 * index and their paths have no common prefix,
-				 * so there cannot be a F/D conflict.
+				 * index.
+				 *
+				 * If there were a conflict with "file", then our
+				 * name would start with "file/" and the last index
+				 * entry would start with "file" but not "file/".
+				 *
+				 * The next character after common prefix is
+				 * not '/', so there can be no conflict.
 				 */
 				return retval;
 			} else {
 				/*
 				 * The entry sorts AFTER the last one in the
-				 * index, but has a common prefix.  Fall through
-				 * to the loop below to disect the entry's path
-				 * and see where the difference is.
+				 * index, and the next character after common
+				 * prefix is '/'.
+				 *
+				 * Either the last index entry is a file in
+				 * conflict with this entry, or it has a name
+				 * which sorts between this entry and the
+				 * potential conflicting file.
+				 *
+				 * In both cases, we fall through to the loop
+				 * below and let the regular search code handle it.
 				 */
 			}
 		} else if (cmp_last == 0) {
@@ -1152,53 +1165,6 @@ static int has_dir_name(struct index_state *istate,
 		}
 		len = slash - name;
 
-		if (cmp_last > 0) {
-			/*
-			 * (len + 1) is a directory boundary (including
-			 * the trailing slash).  And since the loop is
-			 * decrementing "slash", the first iteration is
-			 * the longest directory prefix; subsequent
-			 * iterations consider parent directories.
-			 */
-
-			if (len + 1 <= len_eq_last) {
-				/*
-				 * The directory prefix (including the trailing
-				 * slash) also appears as a prefix in the last
-				 * entry, so the remainder cannot collide (because
-				 * strcmp said the whole path was greater).
-				 *
-				 * EQ: last: xxx/A
-				 *     this: xxx/B
-				 *
-				 * LT: last: xxx/file_A
-				 *     this: xxx/file_B
-				 */
-				return retval;
-			}
-
-			if (len > len_eq_last) {
-				/*
-				 * This part of the directory prefix (excluding
-				 * the trailing slash) is longer than the known
-				 * equal portions, so this sub-directory cannot
-				 * collide with a file.
-				 *
-				 * GT: last: xxxA
-				 *     this: xxxB/file
-				 */
-				return retval;
-			}
-
-			/*
-			 * This is a possible collision. Fall through and
-			 * let the regular search code handle it.
-			 *
-			 * last: xxx
-			 * this: xxx/file
-			 */
-		}
-
 		pos = index_name_stage_pos(istate, name, len, stage, EXPAND_SPARSE);
 		if (pos >= 0) {
 			/*
diff --git a/repository.c b/repository.c
index e15b416..da42ff5 100644
--- a/repository.c
+++ b/repository.c
@@ -295,6 +295,8 @@ void repo_clear(struct repository *repo)
 	parsed_object_pool_clear(repo->parsed_objects);
 	FREE_AND_NULL(repo->parsed_objects);
 
+	FREE_AND_NULL(repo->settings.fsmonitor);
+
 	if (repo->config) {
 		git_configset_clear(repo->config);
 		FREE_AND_NULL(repo->config);
diff --git a/sequencer.c b/sequencer.c
index 2c19846..88de4dc 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -207,6 +207,46 @@ static GIT_PATH_FUNC(rebase_path_no_reschedule_failed_exec, "rebase-merge/no-res
 static GIT_PATH_FUNC(rebase_path_drop_redundant_commits, "rebase-merge/drop_redundant_commits")
 static GIT_PATH_FUNC(rebase_path_keep_redundant_commits, "rebase-merge/keep_redundant_commits")
 
+/*
+ * A 'struct replay_ctx' represents the private state of the sequencer.
+ */
+struct replay_ctx {
+	/*
+	 * The commit message that will be used except at the end of a
+	 * chain of fixup and squash commands.
+	 */
+	struct strbuf message;
+	/*
+	 * The list of completed fixup and squash commands in the
+	 * current chain.
+	 */
+	struct strbuf current_fixups;
+	/*
+	 * Stores the reflog message that will be used when creating a
+	 * commit. Points to a static buffer and should not be free()'d.
+	 */
+	const char *reflog_message;
+	/*
+	 * The number of completed fixup and squash commands in the
+	 * current chain.
+	 */
+	int current_fixup_count;
+	/*
+	 * Whether message contains a commit message.
+	 */
+	unsigned have_message :1;
+};
+
+struct replay_ctx* replay_ctx_new(void)
+{
+	struct replay_ctx *ctx = xcalloc(1, sizeof(*ctx));
+
+	strbuf_init(&ctx->current_fixups, 0);
+	strbuf_init(&ctx->message, 0);
+
+	return ctx;
+}
+
 /**
  * A 'struct update_refs_record' represents a value in the update-refs
  * list. We use a string_list to map refs to these (before, after) pairs.
@@ -366,17 +406,26 @@ static const char *gpg_sign_opt_quoted(struct replay_opts *opts)
 	return buf.buf;
 }
 
+static void replay_ctx_release(struct replay_ctx *ctx)
+{
+	strbuf_release(&ctx->current_fixups);
+	strbuf_release(&ctx->message);
+}
+
 void replay_opts_release(struct replay_opts *opts)
 {
+	struct replay_ctx *ctx = opts->ctx;
+
 	free(opts->gpg_sign);
 	free(opts->reflog_action);
 	free(opts->default_strategy);
 	free(opts->strategy);
 	strvec_clear (&opts->xopts);
-	strbuf_release(&opts->current_fixups);
 	if (opts->revs)
 		release_revisions(opts->revs);
 	free(opts->revs);
+	replay_ctx_release(ctx);
+	free(opts->ctx);
 }
 
 int sequencer_remove_state(struct replay_opts *opts)
@@ -1084,6 +1133,7 @@ static int run_git_commit(const char *defmsg,
 			  struct replay_opts *opts,
 			  unsigned int flags)
 {
+	struct replay_ctx *ctx = opts->ctx;
 	struct child_process cmd = CHILD_PROCESS_INIT;
 
 	if ((flags & CLEANUP_MSG) && (flags & VERBATIM_MSG))
@@ -1101,7 +1151,7 @@ static int run_git_commit(const char *defmsg,
 			     gpg_opt, gpg_opt);
 	}
 
-	strvec_pushf(&cmd.env, GIT_REFLOG_ACTION "=%s", opts->reflog_message);
+	strvec_pushf(&cmd.env, GIT_REFLOG_ACTION "=%s", ctx->reflog_message);
 
 	if (opts->committer_date_is_author_date)
 		strvec_pushf(&cmd.env, "GIT_COMMITTER_DATE=%s",
@@ -1487,6 +1537,7 @@ static int try_to_commit(struct repository *r,
 			 struct replay_opts *opts, unsigned int flags,
 			 struct object_id *oid)
 {
+	struct replay_ctx *ctx = opts->ctx;
 	struct object_id tree;
 	struct commit *current_head = NULL;
 	struct commit_list *parents = NULL;
@@ -1648,7 +1699,7 @@ static int try_to_commit(struct repository *r,
 		goto out;
 	}
 
-	if (update_head_with_reflog(current_head, oid, opts->reflog_message,
+	if (update_head_with_reflog(current_head, oid, ctx->reflog_message,
 				    msg, &err)) {
 		res = error("%s", err.buf);
 		goto out;
@@ -1878,10 +1929,10 @@ static void add_commented_lines(struct strbuf *buf, const void *str, size_t len)
 }
 
 /* Does the current fixup chain contain a squash command? */
-static int seen_squash(struct replay_opts *opts)
+static int seen_squash(struct replay_ctx *ctx)
 {
-	return starts_with(opts->current_fixups.buf, "squash") ||
-		strstr(opts->current_fixups.buf, "\nsquash");
+	return starts_with(ctx->current_fixups.buf, "squash") ||
+		strstr(ctx->current_fixups.buf, "\nsquash");
 }
 
 static void update_comment_bufs(struct strbuf *buf1, struct strbuf *buf2, int n)
@@ -1957,6 +2008,7 @@ static int append_squash_message(struct strbuf *buf, const char *body,
 			 enum todo_command command, struct replay_opts *opts,
 			 unsigned flag)
 {
+	struct replay_ctx *ctx = opts->ctx;
 	const char *fixup_msg;
 	size_t commented_len = 0, fixup_off;
 	/*
@@ -1965,13 +2017,13 @@ static int append_squash_message(struct strbuf *buf, const char *body,
 	 * squashing commit messages.
 	 */
 	if (starts_with(body, "amend!") ||
-	    ((command == TODO_SQUASH || seen_squash(opts)) &&
+	    ((command == TODO_SQUASH || seen_squash(ctx)) &&
 	     (starts_with(body, "squash!") || starts_with(body, "fixup!"))))
 		commented_len = commit_subject_length(body);
 
 	strbuf_addf(buf, "\n%s ", comment_line_str);
 	strbuf_addf(buf, _(nth_commit_msg_fmt),
-		    ++opts->current_fixup_count + 1);
+		    ++ctx->current_fixup_count + 1);
 	strbuf_addstr(buf, "\n\n");
 	strbuf_add_commented_lines(buf, body, commented_len, comment_line_str);
 	/* buf->buf may be reallocated so store an offset into the buffer */
@@ -1979,7 +2031,7 @@ static int append_squash_message(struct strbuf *buf, const char *body,
 	strbuf_addstr(buf, body + commented_len);
 
 	/* fixup -C after squash behaves like squash */
-	if (is_fixup_flag(command, flag) && !seen_squash(opts)) {
+	if (is_fixup_flag(command, flag) && !seen_squash(ctx)) {
 		/*
 		 * We're replacing the commit message so we need to
 		 * append the Signed-off-by: trailer if the user
@@ -2013,12 +2065,13 @@ static int update_squash_messages(struct repository *r,
 				  struct replay_opts *opts,
 				  unsigned flag)
 {
+	struct replay_ctx *ctx = opts->ctx;
 	struct strbuf buf = STRBUF_INIT;
 	int res = 0;
 	const char *message, *body;
 	const char *encoding = get_commit_output_encoding();
 
-	if (opts->current_fixup_count > 0) {
+	if (ctx->current_fixup_count > 0) {
 		struct strbuf header = STRBUF_INIT;
 		char *eol;
 
@@ -2031,10 +2084,10 @@ static int update_squash_messages(struct repository *r,
 
 		strbuf_addf(&header, "%s ", comment_line_str);
 		strbuf_addf(&header, _(combined_commit_msg_fmt),
-			    opts->current_fixup_count + 2);
+			    ctx->current_fixup_count + 2);
 		strbuf_splice(&buf, 0, eol - buf.buf, header.buf, header.len);
 		strbuf_release(&header);
-		if (is_fixup_flag(command, flag) && !seen_squash(opts))
+		if (is_fixup_flag(command, flag) && !seen_squash(ctx))
 			update_squash_message_for_fixup(&buf);
 	} else {
 		struct object_id head;
@@ -2081,7 +2134,7 @@ static int update_squash_messages(struct repository *r,
 	} else if (command == TODO_FIXUP) {
 		strbuf_addf(&buf, "\n%s ", comment_line_str);
 		strbuf_addf(&buf, _(skip_nth_commit_msg_fmt),
-			    ++opts->current_fixup_count + 1);
+			    ++ctx->current_fixup_count + 1);
 		strbuf_addstr(&buf, "\n\n");
 		strbuf_add_commented_lines(&buf, body, strlen(body),
 					   comment_line_str);
@@ -2095,12 +2148,12 @@ static int update_squash_messages(struct repository *r,
 	strbuf_release(&buf);
 
 	if (!res) {
-		strbuf_addf(&opts->current_fixups, "%s%s %s",
-			    opts->current_fixups.len ? "\n" : "",
+		strbuf_addf(&ctx->current_fixups, "%s%s %s",
+			    ctx->current_fixups.len ? "\n" : "",
 			    command_to_string(command),
 			    oid_to_hex(&commit->object.oid));
-		res = write_message(opts->current_fixups.buf,
-				    opts->current_fixups.len,
+		res = write_message(ctx->current_fixups.buf,
+				    ctx->current_fixups.len,
 				    rebase_path_current_fixups(), 0);
 	}
 
@@ -2178,6 +2231,7 @@ static int do_pick_commit(struct repository *r,
 			  struct replay_opts *opts,
 			  int final_fixup, int *check_todo)
 {
+	struct replay_ctx *ctx = opts->ctx;
 	unsigned int flags = should_edit(opts) ? EDIT_MSG : 0;
 	const char *msg_file = should_edit(opts) ? NULL : git_path_merge_msg(r);
 	struct object_id head;
@@ -2185,7 +2239,6 @@ static int do_pick_commit(struct repository *r,
 	const char *base_label, *next_label;
 	char *author = NULL;
 	struct commit_message msg = { NULL, NULL, NULL, NULL };
-	struct strbuf msgbuf = STRBUF_INIT;
 	int res, unborn = 0, reword = 0, allow, drop_commit;
 	enum todo_command command = item->command;
 	struct commit *commit = item->commit;
@@ -2284,7 +2337,7 @@ static int do_pick_commit(struct repository *r,
 		next = parent;
 		next_label = msg.parent_label;
 		if (opts->commit_use_reference) {
-			strbuf_addstr(&msgbuf,
+			strbuf_addstr(&ctx->message,
 				"# *** SAY WHY WE ARE REVERTING ON THE TITLE LINE ***");
 		} else if (skip_prefix(msg.subject, "Revert \"", &orig_subject) &&
 			   /*
@@ -2293,21 +2346,21 @@ static int do_pick_commit(struct repository *r,
 			    * thus requiring excessive complexity to deal with.
 			    */
 			   !starts_with(orig_subject, "Revert \"")) {
-			strbuf_addstr(&msgbuf, "Reapply \"");
-			strbuf_addstr(&msgbuf, orig_subject);
+			strbuf_addstr(&ctx->message, "Reapply \"");
+			strbuf_addstr(&ctx->message, orig_subject);
 		} else {
-			strbuf_addstr(&msgbuf, "Revert \"");
-			strbuf_addstr(&msgbuf, msg.subject);
-			strbuf_addstr(&msgbuf, "\"");
+			strbuf_addstr(&ctx->message, "Revert \"");
+			strbuf_addstr(&ctx->message, msg.subject);
+			strbuf_addstr(&ctx->message, "\"");
 		}
-		strbuf_addstr(&msgbuf, "\n\nThis reverts commit ");
-		refer_to_commit(opts, &msgbuf, commit);
+		strbuf_addstr(&ctx->message, "\n\nThis reverts commit ");
+		refer_to_commit(opts, &ctx->message, commit);
 
 		if (commit->parents && commit->parents->next) {
-			strbuf_addstr(&msgbuf, ", reversing\nchanges made to ");
-			refer_to_commit(opts, &msgbuf, parent);
+			strbuf_addstr(&ctx->message, ", reversing\nchanges made to ");
+			refer_to_commit(opts, &ctx->message, parent);
 		}
-		strbuf_addstr(&msgbuf, ".\n");
+		strbuf_addstr(&ctx->message, ".\n");
 	} else {
 		const char *p;
 
@@ -2316,21 +2369,22 @@ static int do_pick_commit(struct repository *r,
 		next = commit;
 		next_label = msg.label;
 
-		/* Append the commit log message to msgbuf. */
+		/* Append the commit log message to ctx->message. */
 		if (find_commit_subject(msg.message, &p))
-			strbuf_addstr(&msgbuf, p);
+			strbuf_addstr(&ctx->message, p);
 
 		if (opts->record_origin) {
-			strbuf_complete_line(&msgbuf);
-			if (!has_conforming_footer(&msgbuf, NULL, 0))
-				strbuf_addch(&msgbuf, '\n');
-			strbuf_addstr(&msgbuf, cherry_picked_prefix);
-			strbuf_addstr(&msgbuf, oid_to_hex(&commit->object.oid));
-			strbuf_addstr(&msgbuf, ")\n");
+			strbuf_complete_line(&ctx->message);
+			if (!has_conforming_footer(&ctx->message, NULL, 0))
+				strbuf_addch(&ctx->message, '\n');
+			strbuf_addstr(&ctx->message, cherry_picked_prefix);
+			strbuf_addstr(&ctx->message, oid_to_hex(&commit->object.oid));
+			strbuf_addstr(&ctx->message, ")\n");
 		}
 		if (!is_fixup(command))
 			author = get_author(msg.message);
 	}
+	ctx->have_message = 1;
 
 	if (command == TODO_REWORD)
 		reword = 1;
@@ -2361,7 +2415,7 @@ static int do_pick_commit(struct repository *r,
 	}
 
 	if (opts->signoff && !is_fixup(command))
-		append_signoff(&msgbuf, 0, 0);
+		append_signoff(&ctx->message, 0, 0);
 
 	if (is_rebase_i(opts) && write_author_script(msg.message) < 0)
 		res = -1;
@@ -2370,17 +2424,17 @@ static int do_pick_commit(struct repository *r,
 		 !strcmp(opts->strategy, "ort") ||
 		 command == TODO_REVERT) {
 		res = do_recursive_merge(r, base, next, base_label, next_label,
-					 &head, &msgbuf, opts);
+					 &head, &ctx->message, opts);
 		if (res < 0)
 			goto leave;
 
-		res |= write_message(msgbuf.buf, msgbuf.len,
+		res |= write_message(ctx->message.buf, ctx->message.len,
 				     git_path_merge_msg(r), 0);
 	} else {
 		struct commit_list *common = NULL;
 		struct commit_list *remotes = NULL;
 
-		res = write_message(msgbuf.buf, msgbuf.len,
+		res = write_message(ctx->message.buf, ctx->message.len,
 				    git_path_merge_msg(r), 0);
 
 		commit_list_insert(base, &common);
@@ -2458,14 +2512,13 @@ static int do_pick_commit(struct repository *r,
 		unlink(rebase_path_fixup_msg());
 		unlink(rebase_path_squash_msg());
 		unlink(rebase_path_current_fixups());
-		strbuf_reset(&opts->current_fixups);
-		opts->current_fixup_count = 0;
+		strbuf_reset(&ctx->current_fixups);
+		ctx->current_fixup_count = 0;
 	}
 
 leave:
 	free_message(commit, &msg);
 	free(author);
-	strbuf_release(&msgbuf);
 	update_abort_safety_file();
 
 	return res;
@@ -2846,12 +2899,14 @@ void sequencer_post_commit_cleanup(struct repository *r, int verbose)
 			NULL, REF_NO_DEREF);
 
 	if (!need_cleanup)
-		return;
+		goto out;
 
 	if (!have_finished_the_last_pick())
-		return;
+		goto out;
 
 	sequencer_remove_state(&opts);
+out:
+	replay_opts_release(&opts);
 }
 
 static void todo_list_write_total_nr(struct todo_list *todo_list)
@@ -3022,6 +3077,8 @@ static void read_strategy_opts(struct replay_opts *opts, struct strbuf *buf)
 
 static int read_populate_opts(struct replay_opts *opts)
 {
+	struct replay_ctx *ctx = opts->ctx;
+
 	if (is_rebase_i(opts)) {
 		struct strbuf buf = STRBUF_INIT;
 		int ret = 0;
@@ -3081,13 +3138,13 @@ static int read_populate_opts(struct replay_opts *opts)
 		read_strategy_opts(opts, &buf);
 		strbuf_reset(&buf);
 
-		if (read_oneliner(&opts->current_fixups,
+		if (read_oneliner(&ctx->current_fixups,
 				  rebase_path_current_fixups(),
 				  READ_ONELINER_SKIP_IF_EMPTY)) {
-			const char *p = opts->current_fixups.buf;
-			opts->current_fixup_count = 1;
+			const char *p = ctx->current_fixups.buf;
+			ctx->current_fixup_count = 1;
 			while ((p = strchr(p, '\n'))) {
-				opts->current_fixup_count++;
+				ctx->current_fixup_count++;
 				p++;
 			}
 		}
@@ -3608,13 +3665,24 @@ static int error_with_patch(struct repository *r,
 			    struct replay_opts *opts,
 			    int exit_code, int to_amend)
 {
-	if (commit) {
-		if (make_patch(r, commit, opts))
+	struct replay_ctx *ctx = opts->ctx;
+
+	/*
+	 * Write the commit message to be used by "git rebase
+	 * --continue". If a "fixup" or "squash" command has conflicts
+	 * then we will have already written rebase_path_message() in
+	 * error_failed_squash(). If an "edit" command was
+	 * fast-forwarded then we don't have a message in ctx->message
+	 * and rely on make_patch() to write rebase_path_message()
+	 * instead.
+	 */
+	if (ctx->have_message && !file_exists(rebase_path_message()) &&
+	    write_message(ctx->message.buf, ctx->message.len,
+			  rebase_path_message(), 0))
+		return error(_("could not write commit message file"));
+
+	if (commit && make_patch(r, commit, opts))
 			return -1;
-	} else if (copy_file(rebase_path_message(),
-			     git_path_merge_msg(r), 0666))
-		return error(_("unable to copy '%s' to '%s'"),
-			     git_path_merge_msg(r), rebase_path_message());
 
 	if (to_amend) {
 		if (intend_to_amend())
@@ -3936,6 +4004,7 @@ static int do_merge(struct repository *r,
 		    const char *arg, int arg_len,
 		    int flags, int *check_todo, struct replay_opts *opts)
 {
+	struct replay_ctx *ctx = opts->ctx;
 	int run_commit_flags = 0;
 	struct strbuf ref_name = STRBUF_INIT;
 	struct commit *head_commit, *merge_commit, *i;
@@ -4064,39 +4133,30 @@ static int do_merge(struct repository *r,
 		write_author_script(message);
 		find_commit_subject(message, &body);
 		len = strlen(body);
-		ret = write_message(body, len, git_path_merge_msg(r), 0);
+		strbuf_add(&ctx->message, body, len);
 		repo_unuse_commit_buffer(r, commit, message);
-		if (ret) {
-			error_errno(_("could not write '%s'"),
-				    git_path_merge_msg(r));
-			goto leave_merge;
-		}
 	} else {
 		struct strbuf buf = STRBUF_INIT;
-		int len;
 
 		strbuf_addf(&buf, "author %s", git_author_info(0));
 		write_author_script(buf.buf);
-		strbuf_reset(&buf);
+		strbuf_release(&buf);
 
 		if (oneline_offset < arg_len) {
-			p = arg + oneline_offset;
-			len = arg_len - oneline_offset;
+			strbuf_add(&ctx->message, arg + oneline_offset,
+				   arg_len - oneline_offset);
 		} else {
-			strbuf_addf(&buf, "Merge %s '%.*s'",
+			strbuf_addf(&ctx->message, "Merge %s '%.*s'",
 				    to_merge->next ? "branches" : "branch",
 				    merge_arg_len, arg);
-			p = buf.buf;
-			len = buf.len;
 		}
-
-		ret = write_message(p, len, git_path_merge_msg(r), 0);
-		strbuf_release(&buf);
-		if (ret) {
-			error_errno(_("could not write '%s'"),
-				    git_path_merge_msg(r));
-			goto leave_merge;
-		}
+	}
+	ctx->have_message = 1;
+	if (write_message(ctx->message.buf, ctx->message.len,
+			  git_path_merge_msg(r), 0)) {
+		    ret = error_errno(_("could not write '%s'"),
+				      git_path_merge_msg(r));
+		    goto leave_merge;
 	}
 
 	if (strategy || to_merge->next) {
@@ -4758,11 +4818,12 @@ static int pick_one_commit(struct repository *r,
 			   struct replay_opts *opts,
 			   int *check_todo, int* reschedule)
 {
+	struct replay_ctx *ctx = opts->ctx;
 	int res;
 	struct todo_item *item = todo_list->items + todo_list->current;
 	const char *arg = todo_item_get_arg(todo_list, item);
 	if (is_rebase_i(opts))
-		opts->reflog_message = reflog_message(
+		ctx->reflog_message = reflog_message(
 			opts, command_to_string(item->command), NULL);
 
 	res = do_pick_commit(r, item, opts, is_final_fixup(todo_list),
@@ -4819,9 +4880,10 @@ static int pick_commits(struct repository *r,
 			struct todo_list *todo_list,
 			struct replay_opts *opts)
 {
+	struct replay_ctx *ctx = opts->ctx;
 	int res = 0, reschedule = 0;
 
-	opts->reflog_message = sequencer_reflog_action(opts);
+	ctx->reflog_message = sequencer_reflog_action(opts);
 	if (opts->allow_ff)
 		assert(!(opts->signoff || opts->no_commit ||
 			 opts->record_origin || should_edit(opts) ||
@@ -4871,6 +4933,8 @@ static int pick_commits(struct repository *r,
 				return stopped_at_head(r);
 			}
 		}
+		strbuf_reset(&ctx->message);
+		ctx->have_message = 0;
 		if (item->command <= TODO_SQUASH) {
 			res = pick_one_commit(r, todo_list, opts, &check_todo,
 					      &reschedule);
@@ -5076,6 +5140,7 @@ static int commit_staged_changes(struct repository *r,
 				 struct replay_opts *opts,
 				 struct todo_list *todo_list)
 {
+	struct replay_ctx *ctx = opts->ctx;
 	unsigned int flags = ALLOW_EMPTY | EDIT_MSG;
 	unsigned int final_fixup = 0, is_clean;
 
@@ -5112,7 +5177,7 @@ static int commit_staged_changes(struct repository *r,
 		 * the commit message and if there was a squash, let the user
 		 * edit it.
 		 */
-		if (!is_clean || !opts->current_fixup_count)
+		if (!is_clean || !ctx->current_fixup_count)
 			; /* this is not the final fixup */
 		else if (!oideq(&head, &to_amend) ||
 			 !file_exists(rebase_path_stopped_sha())) {
@@ -5121,20 +5186,20 @@ static int commit_staged_changes(struct repository *r,
 				unlink(rebase_path_fixup_msg());
 				unlink(rebase_path_squash_msg());
 				unlink(rebase_path_current_fixups());
-				strbuf_reset(&opts->current_fixups);
-				opts->current_fixup_count = 0;
+				strbuf_reset(&ctx->current_fixups);
+				ctx->current_fixup_count = 0;
 			}
 		} else {
 			/* we are in a fixup/squash chain */
-			const char *p = opts->current_fixups.buf;
-			int len = opts->current_fixups.len;
+			const char *p = ctx->current_fixups.buf;
+			int len = ctx->current_fixups.len;
 
-			opts->current_fixup_count--;
+			ctx->current_fixup_count--;
 			if (!len)
 				BUG("Incorrect current_fixups:\n%s", p);
 			while (len && p[len - 1] != '\n')
 				len--;
-			strbuf_setlen(&opts->current_fixups, len);
+			strbuf_setlen(&ctx->current_fixups, len);
 			if (write_message(p, len, rebase_path_current_fixups(),
 					  0) < 0)
 				return error(_("could not write file: '%s'"),
@@ -5151,7 +5216,7 @@ static int commit_staged_changes(struct repository *r,
 			 * actually need to re-commit with a cleaned up commit
 			 * message.
 			 */
-			if (opts->current_fixup_count > 0 &&
+			if (ctx->current_fixup_count > 0 &&
 			    !is_fixup(peek_command(todo_list, 0))) {
 				final_fixup = 1;
 				/*
@@ -5224,20 +5289,21 @@ static int commit_staged_changes(struct repository *r,
 		unlink(rebase_path_fixup_msg());
 		unlink(rebase_path_squash_msg());
 	}
-	if (opts->current_fixup_count > 0) {
+	if (ctx->current_fixup_count > 0) {
 		/*
 		 * Whether final fixup or not, we just cleaned up the commit
 		 * message...
 		 */
 		unlink(rebase_path_current_fixups());
-		strbuf_reset(&opts->current_fixups);
-		opts->current_fixup_count = 0;
+		strbuf_reset(&ctx->current_fixups);
+		ctx->current_fixup_count = 0;
 	}
 	return 0;
 }
 
 int sequencer_continue(struct repository *r, struct replay_opts *opts)
 {
+	struct replay_ctx *ctx = opts->ctx;
 	struct todo_list todo_list = TODO_LIST_INIT;
 	int res;
 
@@ -5257,7 +5323,7 @@ int sequencer_continue(struct repository *r, struct replay_opts *opts)
 			unlink(rebase_path_dropped());
 		}
 
-		opts->reflog_message = reflog_message(opts, "continue", NULL);
+		ctx->reflog_message = reflog_message(opts, "continue", NULL);
 		if (commit_staged_changes(r, opts, &todo_list)) {
 			res = -1;
 			goto release_todo_list;
@@ -5309,7 +5375,7 @@ static int single_pick(struct repository *r,
 			TODO_PICK : TODO_REVERT;
 	item.commit = cmit;
 
-	opts->reflog_message = sequencer_reflog_action(opts);
+	opts->ctx->reflog_message = sequencer_reflog_action(opts);
 	return do_pick_commit(r, &item, opts, 0, &check_todo);
 }
 
diff --git a/sequencer.h b/sequencer.h
index 437eabd..a309ddd 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -31,6 +31,9 @@ enum commit_msg_cleanup_mode {
 	COMMIT_MSG_CLEANUP_ALL
 };
 
+struct replay_ctx;
+struct replay_ctx* replay_ctx_new(void);
+
 struct replay_opts {
 	enum replay_action action;
 
@@ -68,10 +71,6 @@ struct replay_opts {
 	/* Reflog */
 	char *reflog_action;
 
-	/* Used by fixup/squash */
-	struct strbuf current_fixups;
-	int current_fixup_count;
-
 	/* placeholder commit for -i --root */
 	struct object_id squash_onto;
 	int have_squash_onto;
@@ -80,13 +79,13 @@ struct replay_opts {
 	struct rev_info *revs;
 
 	/* Private use */
-	const char *reflog_message;
+	struct replay_ctx *ctx;
 };
 #define REPLAY_OPTS_INIT {			\
 	.edit = -1,				\
 	.action = -1,				\
-	.current_fixups = STRBUF_INIT,		\
 	.xopts = STRVEC_INIT,			\
+	.ctx = replay_ctx_new(),		\
 }
 
 /*
diff --git a/setup.c b/setup.c
index f4b32f7..30f243f 100644
--- a/setup.c
+++ b/setup.c
@@ -16,6 +16,7 @@
 #include "quote.h"
 #include "trace2.h"
 #include "worktree.h"
+#include "exec-cmd.h"
 
 static int inside_git_dir = -1;
 static int inside_work_tree = -1;
@@ -1220,6 +1221,27 @@ static int ensure_valid_ownership(const char *gitfile,
 	return data.is_safe;
 }
 
+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)
@@ -1781,6 +1803,57 @@ int daemonize(void)
 #endif
 }
 
+struct template_dir_cb_data {
+	char *path;
+	int initialized;
+};
+
+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
@@ -1856,8 +1929,9 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,
 	}
 }
 
-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;
@@ -1866,16 +1940,8 @@ static void copy_templates(const char *template_dir, const char *init_template_d
 	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, '/');
@@ -2023,7 +2089,6 @@ static int create_default_files(const char *template_path,
 	char *path;
 	int reinit;
 	int filemode;
-	const char *init_template_dir = NULL;
 	const char *work_tree = get_git_work_tree();
 
 	/*
@@ -2035,9 +2100,7 @@ static int create_default_files(const char *template_path,
 	 * 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);
diff --git a/setup.h b/setup.h
index d88bb37..b3fd3bf 100644
--- a/setup.h
+++ b/setup.h
@@ -41,6 +41,18 @@ const char *read_gitfile_gently(const char *path, int *return_error_code);
 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);
 
 /*
@@ -172,6 +184,8 @@ int verify_repository_format(const struct repository_format *format,
  */
 void check_repository_format(struct repository_format *fmt);
 
+const char *get_template_dir(const char *option_template);
+
 #define INIT_DB_QUIET      (1 << 0)
 #define INIT_DB_EXIST_OK   (1 << 1)
 #define INIT_DB_SKIP_REFDB (1 << 2)
diff --git a/submodule.c b/submodule.c
index ce2d032..05d6db9 100644
--- a/submodule.c
+++ b/submodule.c
@@ -1015,6 +1015,9 @@ static int submodule_has_commits(struct repository *r,
 		.super_oid = super_oid
 	};
 
+	if (validate_submodule_path(path) < 0)
+		exit(128);
+
 	oid_array_for_each_unique(commits, check_has_commit, &has_commit);
 
 	if (has_commit.result) {
@@ -1137,6 +1140,9 @@ static int push_submodule(const char *path,
 			  const struct string_list *push_options,
 			  int dry_run)
 {
+	if (validate_submodule_path(path) < 0)
+		exit(128);
+
 	if (for_each_remote_ref_submodule(path, has_remote, NULL) > 0) {
 		struct child_process cp = CHILD_PROCESS_INIT;
 		strvec_push(&cp.args, "push");
@@ -1186,6 +1192,9 @@ static void submodule_push_check(const char *path, const char *head,
 	struct child_process cp = CHILD_PROCESS_INIT;
 	int i;
 
+	if (validate_submodule_path(path) < 0)
+		exit(128);
+
 	strvec_push(&cp.args, "submodule--helper");
 	strvec_push(&cp.args, "push-check");
 	strvec_push(&cp.args, head);
@@ -1517,6 +1526,9 @@ static struct fetch_task *fetch_task_create(struct submodule_parallel_fetch *spf
 	struct fetch_task *task = xmalloc(sizeof(*task));
 	memset(task, 0, sizeof(*task));
 
+	if (validate_submodule_path(path) < 0)
+		exit(128);
+
 	task->sub = submodule_from_path(spf->r, treeish_name, path);
 
 	if (!task->sub) {
@@ -1878,6 +1890,9 @@ unsigned is_submodule_modified(const char *path, int ignore_untracked)
 	const char *git_dir;
 	int ignore_cp_exit_code = 0;
 
+	if (validate_submodule_path(path) < 0)
+		exit(128);
+
 	strbuf_addf(&buf, "%s/.git", path);
 	git_dir = read_gitfile(buf.buf);
 	if (!git_dir)
@@ -1954,6 +1969,9 @@ int submodule_uses_gitfile(const char *path)
 	struct strbuf buf = STRBUF_INIT;
 	const char *git_dir;
 
+	if (validate_submodule_path(path) < 0)
+		exit(128);
+
 	strbuf_addf(&buf, "%s/.git", path);
 	git_dir = read_gitfile(buf.buf);
 	if (!git_dir) {
@@ -1993,6 +2011,9 @@ int bad_to_remove_submodule(const char *path, unsigned flags)
 	struct strbuf buf = STRBUF_INIT;
 	int ret = 0;
 
+	if (validate_submodule_path(path) < 0)
+		exit(128);
+
 	if (!file_exists(path) || is_empty_dir(path))
 		return 0;
 
@@ -2043,6 +2064,9 @@ void submodule_unset_core_worktree(const struct submodule *sub)
 {
 	struct strbuf config_path = STRBUF_INIT;
 
+	if (validate_submodule_path(sub->path) < 0)
+		exit(128);
+
 	submodule_name_to_gitdir(&config_path, the_repository, sub->name);
 	strbuf_addstr(&config_path, "/config");
 
@@ -2057,6 +2081,9 @@ static int submodule_has_dirty_index(const struct submodule *sub)
 {
 	struct child_process cp = CHILD_PROCESS_INIT;
 
+	if (validate_submodule_path(sub->path) < 0)
+		exit(128);
+
 	prepare_submodule_repo_env(&cp.env);
 
 	cp.git_cmd = 1;
@@ -2074,6 +2101,10 @@ static int submodule_has_dirty_index(const struct submodule *sub)
 static void submodule_reset_index(const char *path, const char *super_prefix)
 {
 	struct child_process cp = CHILD_PROCESS_INIT;
+
+	if (validate_submodule_path(path) < 0)
+		exit(128);
+
 	prepare_submodule_repo_env(&cp.env);
 
 	cp.git_cmd = 1;
@@ -2137,10 +2168,27 @@ int submodule_move_head(const char *path, const char *super_prefix,
 			if (!submodule_uses_gitfile(path))
 				absorb_git_dir_into_superproject(path,
 								 super_prefix);
+			else {
+				char *dotgit = xstrfmt("%s/.git", path);
+				char *git_dir = xstrdup(read_gitfile(dotgit));
+
+				free(dotgit);
+				if (validate_submodule_git_dir(git_dir,
+							       sub->name) < 0)
+					die(_("refusing to create/use '%s' in "
+					      "another submodule's git dir"),
+					    git_dir);
+				free(git_dir);
+			}
 		} else {
 			struct strbuf gitdir = STRBUF_INIT;
 			submodule_name_to_gitdir(&gitdir, the_repository,
 						 sub->name);
+			if (validate_submodule_git_dir(gitdir.buf,
+						       sub->name) < 0)
+				die(_("refusing to create/use '%s' in another "
+				      "submodule's git dir"),
+				    gitdir.buf);
 			connect_work_tree_and_git_dir(path, gitdir.buf, 0);
 			strbuf_release(&gitdir);
 
@@ -2261,6 +2309,34 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)
 	return 0;
 }
 
+int validate_submodule_path(const char *path)
+{
+	char *p = xstrdup(path);
+	struct stat st;
+	int i, ret = 0;
+	char sep;
+
+	for (i = 0; !ret && p[i]; i++) {
+		if (!is_dir_sep(p[i]))
+			continue;
+
+		sep = p[i];
+		p[i] = '\0';
+		/* allow missing components, but no symlinks */
+		ret = lstat(p, &st) || !S_ISLNK(st.st_mode) ? 0 : -1;
+		p[i] = sep;
+		if (ret)
+			error(_("expected '%.*s' in submodule path '%s' not to "
+				"be a symbolic link"), i, p, p);
+	}
+	if (!lstat(p, &st) && S_ISLNK(st.st_mode))
+		ret = error(_("expected submodule path '%s' not to be a "
+			      "symbolic link"), p);
+	free(p);
+	return ret;
+}
+
+
 /*
  * Embeds a single submodules git directory into the superprojects git dir,
  * non recursively.
@@ -2272,6 +2348,9 @@ static void relocate_single_git_dir_into_superproject(const char *path,
 	struct strbuf new_gitdir = STRBUF_INIT;
 	const struct submodule *sub;
 
+	if (validate_submodule_path(path) < 0)
+		exit(128);
+
 	if (submodule_uses_worktrees(path))
 		die(_("relocate_gitdir for submodule '%s' with "
 		      "more than one worktree not supported"), path);
@@ -2313,6 +2392,9 @@ static void absorb_git_dir_into_superproject_recurse(const char *path,
 
 	struct child_process cp = CHILD_PROCESS_INIT;
 
+	if (validate_submodule_path(path) < 0)
+		exit(128);
+
 	cp.dir = path;
 	cp.git_cmd = 1;
 	cp.no_stdin = 1;
@@ -2337,6 +2419,10 @@ void absorb_git_dir_into_superproject(const char *path,
 	int err_code;
 	const char *sub_git_dir;
 	struct strbuf gitdir = STRBUF_INIT;
+
+	if (validate_submodule_path(path) < 0)
+		exit(128);
+
 	strbuf_addf(&gitdir, "%s/.git", path);
 	sub_git_dir = resolve_gitdir_gently(gitdir.buf, &err_code);
 
@@ -2479,6 +2565,9 @@ int submodule_to_gitdir(struct strbuf *buf, const char *submodule)
 	const char *git_dir;
 	int ret = 0;
 
+	if (validate_submodule_path(submodule) < 0)
+		exit(128);
+
 	strbuf_reset(buf);
 	strbuf_addstr(buf, submodule);
 	strbuf_complete(buf, '/');
diff --git a/submodule.h b/submodule.h
index c55a25c..b50d29e 100644
--- a/submodule.h
+++ b/submodule.h
@@ -148,6 +148,11 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,
  */
 int validate_submodule_git_dir(char *git_dir, const char *submodule_name);
 
+/*
+ * Make sure that the given submodule path does not follow symlinks.
+ */
+int validate_submodule_path(const char *path);
+
 #define SUBMODULE_MOVE_HEAD_DRY_RUN (1<<0)
 #define SUBMODULE_MOVE_HEAD_FORCE   (1<<1)
 int submodule_move_head(const char *path, const char *super_prefix,
diff --git a/t/helper/test-path-utils.c b/t/helper/test-path-utils.c
index 70396fa..bf0e23e 100644
--- a/t/helper/test-path-utils.c
+++ b/t/helper/test-path-utils.c
@@ -7,6 +7,7 @@
 #include "string-list.h"
 #include "trace.h"
 #include "utf8.h"
+#include "copy.h"
 
 /*
  * A "string_list_each_func_t" function that normalizes an entry from
diff --git a/t/lib-chunk.sh b/t/lib-chunk.sh
index a7cd9c3..9f01df1 100644
--- a/t/lib-chunk.sh
+++ b/t/lib-chunk.sh
@@ -13,5 +13,6 @@
 	fn=$1; shift
 	perl "$TEST_DIRECTORY"/lib-chunk/corrupt-chunk-file.pl \
 		"$@" <"$fn" >"$fn.tmp" &&
-	mv "$fn.tmp" "$fn"
+	# some vintages of macOS 'mv' fails to overwrite a read-only file.
+	mv -f "$fn.tmp" "$fn"
 }
diff --git a/t/t0000-basic.sh b/t/t0000-basic.sh
index 6e300be..98b81e4 100755
--- a/t/t0000-basic.sh
+++ b/t/t0000-basic.sh
@@ -1201,6 +1201,34 @@
 	test $len = 4098
 '
 
+# D/F conflict checking uses an optimization when adding to the end.
+# make sure it does not get confused by `a-` sorting _between_
+# `a` and `a/`.
+test_expect_success 'more update-index D/F conflicts' '
+	# empty the index to make sure our entry is last
+	git read-tree --empty &&
+	cacheinfo=100644,$(test_oid empty_blob) &&
+	git update-index --add --cacheinfo $cacheinfo,path5/a &&
+
+	test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/file &&
+	test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/file &&
+	test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/c/file &&
+
+	# "a-" sorts between "a" and "a/"
+	git update-index --add --cacheinfo $cacheinfo,path5/a- &&
+
+	test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/file &&
+	test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/file &&
+	test_must_fail git update-index --add --cacheinfo $cacheinfo,path5/a/b/c/file &&
+
+	cat >expected <<-\EOF &&
+	path5/a
+	path5/a-
+	EOF
+	git ls-files >actual &&
+	test_cmp expected actual
+'
+
 test_expect_success 'test_must_fail on a failing git command' '
 	test_must_fail git notacommand
 '
diff --git a/t/t0003-attributes.sh b/t/t0003-attributes.sh
index 774b52c..71f0828 100755
--- a/t/t0003-attributes.sh
+++ b/t/t0003-attributes.sh
@@ -398,13 +398,19 @@
 	)
 '
 
-test_expect_success 'bare repo defaults to reading .gitattributes from HEAD' '
+test_expect_success 'bare repo no longer defaults to reading .gitattributes from HEAD' '
 	test_when_finished rm -rf test bare_with_gitattribute &&
 	git init test &&
 	test_commit -C test gitattributes .gitattributes "f/path test=val" &&
 	git clone --bare test bare_with_gitattribute &&
-	echo "f/path: test: val" >expect &&
+
+	echo "f/path: test: unspecified" >expect &&
 	git -C bare_with_gitattribute check-attr test -- f/path >actual &&
+	test_cmp expect actual &&
+
+	echo "f/path: test: val" >expect &&
+	git -C bare_with_gitattribute -c attr.tree=HEAD \
+		check-attr test -- f/path >actual &&
 	test_cmp expect actual
 '
 
@@ -572,6 +578,16 @@
 	test_cmp expect err
 '
 
+test_expect_success EXPENSIVE 'large attributes blob ignored' '
+	test_when_finished "git update-index --remove .gitattributes" &&
+	blob=$(dd if=/dev/zero bs=1048576 count=101 2>/dev/null | git hash-object -w --stdin) &&
+	git update-index --add --cacheinfo 100644,$blob,.gitattributes &&
+	tree="$(git write-tree)" &&
+	git check-attr --cached --all --source="$tree" path >/dev/null 2>err &&
+	echo "warning: ignoring overly large gitattributes blob ${SQ}.gitattributes${SQ}" >expect &&
+	test_cmp expect err
+'
+
 test_expect_success 'builtin object mode attributes work (dir and regular paths)' '
 	>normal &&
 	attr_check_object_mode normal 100644 &&
diff --git a/t/t0033-safe-directory.sh b/t/t0033-safe-directory.sh
index dc34968..11c3e8f 100755
--- a/t/t0033-safe-directory.sh
+++ b/t/t0033-safe-directory.sh
@@ -80,4 +80,28 @@
 	git status
 '
 
+test_expect_success 'local clone of unowned repo refused in unsafe directory' '
+	test_when_finished "rm -rf source" &&
+	git init source &&
+	(
+		sane_unset GIT_TEST_ASSUME_DIFFERENT_OWNER &&
+		test_commit -C source initial
+	) &&
+	test_must_fail git clone --local source target &&
+	test_path_is_missing target
+'
+
+test_expect_success 'local clone of unowned repo accepted in safe directory' '
+	test_when_finished "rm -rf source" &&
+	git init source &&
+	(
+		sane_unset GIT_TEST_ASSUME_DIFFERENT_OWNER &&
+		test_commit -C source initial
+	) &&
+	test_must_fail git clone --local source target &&
+	git config --global --add safe.directory "$(pwd)/source/.git" &&
+	git clone --local source target &&
+	test_path_is_dir target
+'
+
 test_done
diff --git a/t/t0068-for-each-repo.sh b/t/t0068-for-each-repo.sh
index 4b90b74..95019e0 100755
--- a/t/t0068-for-each-repo.sh
+++ b/t/t0068-for-each-repo.sh
@@ -59,4 +59,20 @@
 	test_cmp expect actual
 '
 
+test_expect_success '--keep-going' '
+	git config keep.going non-existing &&
+	git config --add keep.going . &&
+
+	test_must_fail git for-each-repo --config=keep.going \
+		-- branch >out 2>err &&
+	test_grep "cannot change to .*non-existing" err &&
+	test_must_be_empty out &&
+
+	test_must_fail git for-each-repo --config=keep.going --keep-going \
+		-- branch >out 2>err &&
+	test_grep "cannot change to .*non-existing" err &&
+	git branch >expect &&
+	test_cmp expect out
+'
+
 test_done
diff --git a/t/t0411-clone-from-partial.sh b/t/t0411-clone-from-partial.sh
new file mode 100755
index 0000000..c98d501
--- /dev/null
+++ b/t/t0411-clone-from-partial.sh
@@ -0,0 +1,78 @@
+#!/bin/sh
+
+test_description='check that local clone does not fetch from promisor remotes'
+
+. ./test-lib.sh
+
+test_expect_success 'create evil repo' '
+	git init tmp &&
+	test_commit -C tmp a &&
+	git -C tmp config uploadpack.allowfilter 1 &&
+	git clone --filter=blob:none --no-local --no-checkout tmp evil &&
+	rm -rf tmp &&
+
+	git -C evil config remote.origin.uploadpack \"\$TRASH_DIRECTORY/fake-upload-pack\" &&
+	write_script fake-upload-pack <<-\EOF &&
+		echo >&2 "fake-upload-pack running"
+		>"$TRASH_DIRECTORY/script-executed"
+		exit 1
+	EOF
+	export TRASH_DIRECTORY &&
+
+	# empty shallow file disables local clone optimization
+	>evil/.git/shallow
+'
+
+test_expect_success 'local clone must not fetch from promisor remote and execute script' '
+	rm -f script-executed &&
+	test_must_fail git clone \
+		--upload-pack="GIT_TEST_ASSUME_DIFFERENT_OWNER=true git-upload-pack" \
+		evil clone1 2>err &&
+	test_grep "detected dubious ownership" err &&
+	test_grep ! "fake-upload-pack running" err &&
+	test_path_is_missing script-executed
+'
+
+test_expect_success 'clone from file://... must not fetch from promisor remote and execute script' '
+	rm -f script-executed &&
+	test_must_fail git clone \
+		--upload-pack="GIT_TEST_ASSUME_DIFFERENT_OWNER=true git-upload-pack" \
+		"file://$(pwd)/evil" clone2 2>err &&
+	test_grep "detected dubious ownership" err &&
+	test_grep ! "fake-upload-pack running" err &&
+	test_path_is_missing script-executed
+'
+
+test_expect_success 'fetch from file://... must not fetch from promisor remote and execute script' '
+	rm -f script-executed &&
+	test_must_fail git fetch \
+		--upload-pack="GIT_TEST_ASSUME_DIFFERENT_OWNER=true git-upload-pack" \
+		"file://$(pwd)/evil" 2>err &&
+	test_grep "detected dubious ownership" err &&
+	test_grep ! "fake-upload-pack running" err &&
+	test_path_is_missing script-executed
+'
+
+test_expect_success 'pack-objects should fetch from promisor remote and execute script' '
+	rm -f script-executed &&
+	echo "HEAD" | test_must_fail git -C evil pack-objects --revs --stdout >/dev/null 2>err &&
+	test_grep "fake-upload-pack running" err &&
+	test_path_is_file script-executed
+'
+
+test_expect_success 'clone from promisor remote does not lazy-fetch by default' '
+	rm -f script-executed &&
+	test_must_fail git clone evil no-lazy 2>err &&
+	test_grep "lazy fetching disabled" err &&
+	test_path_is_missing script-executed
+'
+
+test_expect_success 'promisor lazy-fetching can be re-enabled' '
+	rm -f script-executed &&
+	test_must_fail env GIT_NO_LAZY_FETCH=0 \
+		git clone evil lazy-ok 2>err &&
+	test_grep "fake-upload-pack running" err &&
+	test_path_is_file script-executed
+'
+
+test_done
diff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh
index f6dc83e..45a0492 100755
--- a/t/t1350-config-hooks-path.sh
+++ b/t/t1350-config-hooks-path.sh
@@ -41,4 +41,11 @@
 	test .git/custom-hooks/abc = "$(cat actual)"
 '
 
+test_expect_success 'core.hooksPath=/dev/null' '
+	git clone -c core.hooksPath=/dev/null . no-templates &&
+	value="$(git -C no-templates config --local core.hooksPath)" &&
+	# The Bash used by Git for Windows rewrites `/dev/null` to `nul`
+	{ test /dev/null = "$value" || test nul = "$value"; }
+'
+
 test_done
diff --git a/t/t3428-rebase-signoff.sh b/t/t3428-rebase-signoff.sh
index 1bebd1c..6f57aed 100755
--- a/t/t3428-rebase-signoff.sh
+++ b/t/t3428-rebase-signoff.sh
@@ -5,12 +5,17 @@
 This test runs git rebase --signoff and make sure that it works.
 '
 
-TEST_PASSES_SANITIZE_LEAK=true
 . ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-rebase.sh
 
 test_expect_success 'setup' '
 	git commit --allow-empty -m "Initial empty commit" &&
 	test_commit first file a &&
+	test_commit second file &&
+	git checkout -b conflict-branch first &&
+	test_commit file-2 file-2 &&
+	test_commit conflict file &&
+	test_commit third file &&
 
 	ident="$GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL>" &&
 
@@ -28,6 +33,22 @@
 	Signed-off-by: $ident
 	EOF
 
+	# Expected commit message after conflict resolution for rebase --signoff
+	cat >expected-signed-conflict <<-EOF &&
+	third
+
+	Signed-off-by: $ident
+
+	conflict
+
+	Signed-off-by: $ident
+
+	file-2
+
+	Signed-off-by: $ident
+
+	EOF
+
 	# Expected commit message after rebase without --signoff (or with --no-signoff)
 	cat >expected-unsigned <<-EOF &&
 	first
@@ -39,8 +60,12 @@
 # We configure an alias to do the rebase --signoff so that
 # on the next subtest we can show that --no-signoff overrides the alias
 test_expect_success 'rebase --apply --signoff adds a sign-off line' '
-	git rbs --apply HEAD^ &&
-	test_commit_message HEAD expected-signed
+	test_must_fail git rbs --apply second third &&
+	git checkout --theirs file &&
+	git add file &&
+	git rebase --continue &&
+	git log --format=%B -n3 >actual &&
+	test_cmp expected-signed-conflict actual
 '
 
 test_expect_success 'rebase --no-signoff does not add a sign-off line' '
@@ -51,28 +76,65 @@
 
 test_expect_success 'rebase --exec --signoff adds a sign-off line' '
 	test_when_finished "rm exec" &&
-	git commit --amend -m "first" &&
-	git rebase --exec "touch exec" --signoff HEAD^ &&
+	git rebase --exec "touch exec" --signoff first^ first &&
 	test_path_is_file exec &&
 	test_commit_message HEAD expected-signed
 '
 
 test_expect_success 'rebase --root --signoff adds a sign-off line' '
-	git commit --amend -m "first" &&
+	git checkout first &&
 	git rebase --root --keep-empty --signoff &&
 	test_commit_message HEAD^ expected-initial-signed &&
 	test_commit_message HEAD expected-signed
 '
 
-test_expect_success 'rebase -i --signoff fails' '
-	git commit --amend -m "first" &&
-	git rebase -i --signoff HEAD^ &&
-	test_commit_message HEAD expected-signed
+test_expect_success 'rebase -m --signoff adds a sign-off line' '
+	test_must_fail git rebase -m --signoff second third &&
+	git checkout --theirs file &&
+	git add file &&
+	GIT_EDITOR="sed -n /Conflicts:/,/^\\\$/p >actual" \
+		git rebase --continue &&
+	cat >expect <<-\EOF &&
+	# Conflicts:
+	#	file
+
+	EOF
+	test_cmp expect actual &&
+	git log --format=%B -n3 >actual &&
+	test_cmp expected-signed-conflict actual
 '
 
-test_expect_success 'rebase -m --signoff fails' '
-	git commit --amend -m "first" &&
-	git rebase -m --signoff HEAD^ &&
-	test_commit_message HEAD expected-signed
+test_expect_success 'rebase -i --signoff adds a sign-off line when editing commit' '
+	(
+		set_fake_editor &&
+		FAKE_LINES="edit 1 edit 3 edit 2" \
+			git rebase -i --signoff first third
+	) &&
+	echo a >a &&
+	git add a &&
+	test_must_fail git rebase --continue &&
+	git checkout --ours file &&
+	echo b >a &&
+	git add a file &&
+	git rebase --continue &&
+	echo c >a &&
+	git add a &&
+	git log --format=%B -n3 >actual &&
+	cat >expect <<-EOF &&
+	conflict
+
+	Signed-off-by: $ident
+
+	third
+
+	Signed-off-by: $ident
+
+	file-2
+
+	Signed-off-by: $ident
+
+	EOF
+	test_cmp expect actual
 '
+
 test_done
diff --git a/t/t3434-rebase-i18n.sh b/t/t3434-rebase-i18n.sh
index e6fef69..a4e482d 100755
--- a/t/t3434-rebase-i18n.sh
+++ b/t/t3434-rebase-i18n.sh
@@ -71,7 +71,7 @@
 		git config i18n.commitencoding $new &&
 		test_must_fail git rebase -m main &&
 		test -f .git/rebase-merge/message &&
-		git stripspace <.git/rebase-merge/message >two.t &&
+		git stripspace -s <.git/rebase-merge/message >two.t &&
 		git add two.t &&
 		git rebase --continue &&
 		compare_msg $msgfile $old $new &&
diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index 00db82f..a7f71f8 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -393,6 +393,15 @@
 	test bar,bar4 = $(cat file),$(cat file2)
 '
 
+test_expect_success 'stash --staged with binary file' '
+	printf "\0" >file &&
+	git add file &&
+	git stash --staged &&
+	git stash pop &&
+	printf "\0" >expect &&
+	test_cmp expect file
+'
+
 test_expect_success 'dont assume push with non-option args' '
 	test_must_fail git stash -q drop 2>err &&
 	test_grep -e "subcommand wasn'\''t specified; '\''push'\'' can'\''t be assumed due to unexpected token '\''drop'\''" err
diff --git a/t/t4046-diff-unmerged.sh b/t/t4046-diff-unmerged.sh
index ffaf693..fb8c517 100755
--- a/t/t4046-diff-unmerged.sh
+++ b/t/t4046-diff-unmerged.sh
@@ -20,13 +20,15 @@
 			for t in o x
 			do
 				path="$b$o$t" &&
-				case "$path" in ooo) continue ;; esac &&
-				paths="$paths$path " &&
-				p="	$path" &&
-				case "$b" in x) echo "$m1$p" ;; esac &&
-				case "$o" in x) echo "$m2$p" ;; esac &&
-				case "$t" in x) echo "$m3$p" ;; esac ||
-				return 1
+				if test "$path" != ooo
+				then
+					paths="$paths$path " &&
+					p="	$path" &&
+					case "$b" in x) echo "$m1$p" ;; esac &&
+					case "$o" in x) echo "$m2$p" ;; esac &&
+					case "$t" in x) echo "$m3$p" ;; esac ||
+					return 1
+				fi
 			done
 		done
 	done >ls-files-s.expect &&
diff --git a/t/t5001-archive-attr.sh b/t/t5001-archive-attr.sh
index eaf959d..7310774 100755
--- a/t/t5001-archive-attr.sh
+++ b/t/t5001-archive-attr.sh
@@ -133,7 +133,8 @@
 '
 
 test_expect_success 'git archive with worktree attributes, bare' '
-	(cd bare && git archive --worktree-attributes HEAD) >bare-worktree.tar &&
+	(cd bare &&
+	git -c attr.tree=HEAD archive --worktree-attributes HEAD) >bare-worktree.tar &&
 	(mkdir bare-worktree && cd bare-worktree && "$TAR" xf -) <bare-worktree.tar
 '
 
diff --git a/t/t5326-multi-pack-bitmaps.sh b/t/t5326-multi-pack-bitmaps.sh
index 5d7d321..cc7220b 100755
--- a/t/t5326-multi-pack-bitmaps.sh
+++ b/t/t5326-multi-pack-bitmaps.sh
@@ -434,6 +434,27 @@
 	)
 '
 
+test_expect_success 'do not follow replace objects for MIDX bitmap' '
+	rm -fr repo &&
+	git init repo &&
+	test_when_finished "rm -fr repo" &&
+	(
+		cd repo &&
+
+		test_commit A &&
+		test_commit B &&
+		git checkout --orphan=orphan A &&
+		test_commit orphan &&
+
+		git replace A HEAD &&
+		git repack -ad --write-midx --write-bitmap-index &&
+
+		# generating reachability bitmaps with replace refs
+		# enabled will result in broken clones
+		git clone --no-local --bare . clone.git
+	)
+'
+
 corrupt_file () {
 	chmod a+w "$1" &&
 	printf "bogus" | dd of="$1" bs=1 seek="12" conv=notrunc
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 33d34d5..9441793 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -1252,6 +1252,30 @@
 	test_cmp fatal-expect fatal-actual
 '
 
+test_expect_success SYMLINKS 'clone does not get confused by a D/F conflict' '
+	git init df-conflict &&
+	(
+		cd df-conflict &&
+		ln -s .git a &&
+		git add a &&
+		test_tick &&
+		git commit -m symlink &&
+		test_commit a- &&
+		rm a &&
+		mkdir -p a/hooks &&
+		write_script a/hooks/post-checkout <<-EOF &&
+		echo WHOOPSIE >&2
+		echo whoopsie >"$TRASH_DIRECTORY"/whoops
+		EOF
+		git add a/hooks/post-checkout &&
+		test_tick &&
+		git commit -m post-checkout
+	) &&
+	git clone df-conflict clone 2>err &&
+	test_grep ! WHOOPS err &&
+	test_path_is_missing whoops
+'
+
 . "$TEST_DIRECTORY"/lib-httpd.sh
 start_httpd
 
diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh
index ca43185..cc0b953 100755
--- a/t/t5601-clone.sh
+++ b/t/t5601-clone.sh
@@ -650,6 +650,21 @@
 	test_grep "the following paths have collided" icasefs/warning
 '
 
+test_expect_success CASE_INSENSITIVE_FS,SYMLINKS \
+		'colliding symlink/directory keeps directory' '
+	git init icasefs-colliding-symlink &&
+	(
+		cd icasefs-colliding-symlink &&
+		a=$(printf a | git hash-object -w --stdin) &&
+		printf "100644 %s 0\tA/dir/b\n120000 %s 0\ta\n" $a $a >idx &&
+		git update-index --index-info <idx &&
+		test_tick &&
+		git commit -m initial
+	) &&
+	git clone icasefs-colliding-symlink icasefs-colliding-symlink-clone &&
+	test_file_not_empty icasefs-colliding-symlink-clone/A/dir/b
+'
+
 test_expect_success 'clone with GIT_DEFAULT_HASH' '
 	(
 		sane_unset GIT_DEFAULT_HASH &&
diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh
index 5c4a89d..9814888 100755
--- a/t/t7400-submodule-basic.sh
+++ b/t/t7400-submodule-basic.sh
@@ -1451,4 +1451,35 @@
 	test_must_be_empty actual
 '
 
+test_expect_success '`submodule init` and `init.templateDir`' '
+	mkdir -p tmpl/hooks &&
+	write_script tmpl/hooks/post-checkout <<-EOF &&
+	echo HOOK-RUN >&2
+	echo I was here >hook.run
+	exit 1
+	EOF
+
+	test_config init.templateDir "$(pwd)/tmpl" &&
+	test_when_finished \
+		"git config --global --unset init.templateDir || true" &&
+	(
+		sane_unset GIT_TEMPLATE_DIR &&
+		NO_SET_GIT_TEMPLATE_DIR=t &&
+		export NO_SET_GIT_TEMPLATE_DIR &&
+
+		git config --global init.templateDir "$(pwd)/tmpl" &&
+		test_must_fail git submodule \
+			add "$submodurl" sub-global 2>err &&
+		git config --global --unset init.templateDir &&
+		test_grep HOOK-RUN err &&
+		test_path_is_file sub-global/hook.run &&
+
+		git config init.templateDir "$(pwd)/tmpl" &&
+		git submodule add "$submodurl" sub-local 2>err &&
+		git config --unset init.templateDir &&
+		test_grep ! HOOK-RUN err &&
+		test_path_is_missing sub-local/hook.run
+	)
+'
+
 test_done
diff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh
index 8491b8c..297c6c3 100755
--- a/t/t7406-submodule-update.sh
+++ b/t/t7406-submodule-update.sh
@@ -1202,4 +1202,52 @@
 	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 &&
+	test_grep ! HOOK-RUN err &&
+	test_path_is_missing "$tell_tale_path"
+'
+
 test_done
diff --git a/t/t7423-submodule-symlinks.sh b/t/t7423-submodule-symlinks.sh
new file mode 100755
index 0000000..3d3c7af
--- /dev/null
+++ b/t/t7423-submodule-symlinks.sh
@@ -0,0 +1,67 @@
+#!/bin/sh
+
+test_description='check that submodule operations do not follow symlinks'
+
+. ./test-lib.sh
+
+test_expect_success 'prepare' '
+	git config --global protocol.file.allow always &&
+	test_commit initial &&
+	git init upstream &&
+	test_commit -C upstream upstream submodule_file &&
+	git submodule add ./upstream a/sm &&
+	test_tick &&
+	git commit -m submodule
+'
+
+test_expect_success SYMLINKS 'git submodule update must not create submodule behind symlink' '
+	rm -rf a b &&
+	mkdir b &&
+	ln -s b a &&
+	test_path_is_missing b/sm &&
+	test_must_fail git submodule update &&
+	test_path_is_missing b/sm
+'
+
+test_expect_success SYMLINKS,CASE_INSENSITIVE_FS 'git submodule update must not create submodule behind symlink on case insensitive fs' '
+	rm -rf a b &&
+	mkdir b &&
+	ln -s b A &&
+	test_must_fail git submodule update &&
+	test_path_is_missing b/sm
+'
+
+prepare_symlink_to_repo() {
+	rm -rf a &&
+	mkdir a &&
+	git init a/target &&
+	git -C a/target fetch ../../upstream &&
+	ln -s target a/sm
+}
+
+test_expect_success SYMLINKS 'git restore --recurse-submodules must not be confused by a symlink' '
+	prepare_symlink_to_repo &&
+	test_must_fail git restore --recurse-submodules a/sm &&
+	test_path_is_missing a/sm/submodule_file &&
+	test_path_is_dir a/target/.git &&
+	test_path_is_missing a/target/submodule_file
+'
+
+test_expect_success SYMLINKS 'git restore --recurse-submodules must not migrate git dir of symlinked repo' '
+	prepare_symlink_to_repo &&
+	rm -rf .git/modules &&
+	test_must_fail git restore --recurse-submodules a/sm &&
+	test_path_is_dir a/target/.git &&
+	test_path_is_missing .git/modules/a/sm &&
+	test_path_is_missing a/target/submodule_file
+'
+
+test_expect_success SYMLINKS 'git checkout -f --recurse-submodules must not migrate git dir of symlinked repo when removing submodule' '
+	prepare_symlink_to_repo &&
+	rm -rf .git/modules &&
+	test_must_fail git checkout -f --recurse-submodules initial &&
+	test_path_is_dir a/target/.git &&
+	test_path_is_missing .git/modules/a/sm
+'
+
+test_done
diff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh
index 46d4fb0..4a9c22c 100755
--- a/t/t7450-bad-git-dotfiles.sh
+++ b/t/t7450-bad-git-dotfiles.sh
@@ -320,7 +320,7 @@
 	fi
 '
 
-test_expect_success 'git dirs of sibling submodules must not be nested' '
+test_expect_success 'setup submodules with nested git dirs' '
 	git init nested &&
 	test_commit -C nested nested &&
 	(
@@ -338,9 +338,39 @@
 		git add .gitmodules thing1 thing2 &&
 		test_tick &&
 		git commit -m nested
-	) &&
+	)
+'
+
+test_expect_success 'git dirs of sibling submodules must not be nested' '
 	test_must_fail git clone --recurse-submodules nested clone 2>err &&
 	test_grep "is inside git dir" err
 '
 
+test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '
+	test_must_fail git clone --recurse-submodules --jobs=2 nested clone_parallel 2>err &&
+	cat err &&
+	grep -E "(already exists|is inside git dir|not a git repository)" err &&
+	{
+		test_path_is_missing .git/modules/hippo/HEAD ||
+		test_path_is_missing .git/modules/hippo/hooks/HEAD
+	}
+'
+
+test_expect_success 'checkout -f --recurse-submodules must not use a nested gitdir' '
+	git clone nested nested_checkout &&
+	(
+		cd nested_checkout &&
+		git submodule init &&
+		git submodule update thing1 &&
+		mkdir -p .git/modules/hippo/hooks/refs &&
+		mkdir -p .git/modules/hippo/hooks/objects/info &&
+		echo "../../../../objects" >.git/modules/hippo/hooks/objects/info/alternates &&
+		echo "ref: refs/heads/master" >.git/modules/hippo/hooks/HEAD
+	) &&
+	test_must_fail git -C nested_checkout checkout -f --recurse-submodules HEAD 2>err &&
+	cat err &&
+	grep "is inside git dir" err &&
+	test_path_is_missing nested_checkout/thing2/.git
+'
+
 test_done
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index 0943dfa..8595489 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -639,9 +639,9 @@
 	# start registers the repo
 	git config --get --global --fixed-value maintenance.repo "$(pwd)" &&
 
-	grep "for-each-repo --config=maintenance.repo maintenance run --schedule=daily" cron.txt &&
-	grep "for-each-repo --config=maintenance.repo maintenance run --schedule=hourly" cron.txt &&
-	grep "for-each-repo --config=maintenance.repo maintenance run --schedule=weekly" cron.txt
+	grep "for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=daily" cron.txt &&
+	grep "for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=hourly" cron.txt &&
+	grep "for-each-repo --keep-going --config=maintenance.repo maintenance run --schedule=weekly" cron.txt
 '
 
 test_expect_success 'stop from existing schedule' '