{"thread":{"id":"61509","subject":"[PATCH 0/8] Various fixes for v2.45.1 and friends","startedAt":"2024-05-17T23:16:00Z","lastAt":"2024-05-21T23:04:49Z","messageCount":54,"participants":["Johannes Schindelin via GitGitGadget","Junio C Hamano","Johannes Schindelin","Jeff King","brian m. carlson"],"isPatch":true,"patchVersion":1,"patchTotal":8},"messages":[{"id":"495002","messageId":"pull.1732.git.1715987756.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":null,"subject":"[PATCH 0/8] Various fixes for v2.45.1 and friends","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-17T23:15:48Z","receivedAt":"2024-05-17T23:16:00Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"There have been a couple of issues that were reported about v2.45.1, and in\naddition I have noticed some myself:\n\n * a memory leak in the clone protection logic\n * a missed adjustment in the Makefile that leads to an incorrect templates\n   path in v2.39.4, v2.40.2 and v2.41.1 (but not in v2.42.2, ..., v2.45.1)\n * an overzealous core.hooksPath check\n * that Git LFS clone problem where it exits with an error (even if the\n   clone often succeeded...)\n\nThis patch series is based on maint-2.39 to allow for (relatively) easy\nfollow-up versions v2.39.5, ..., v2.45.2.\n\nJohannes Schindelin (8):\n  hook: plug a new memory leak\n  init: use the correct path of the templates directory again\n  Revert \"core.hooksPath: add some protection while cloning\"\n  tests: verify that `clone -c core.hooksPath=/dev/null` works again\n  hook(clone protections): add escape hatch\n  hooks(clone protections): special-case current Git LFS hooks\n  hooks(clone protections): simplify templates hooks validation\n  Revert \"Add a helper function to compare file contents\"\n\n Documentation/config/safe.txt |   6 ++\n Makefile                      |   2 +-\n builtin/init-db.c             |   7 +++\n cache.h                       |  14 -----\n config.c                      |  13 +----\n copy.c                        |  58 --------------------\n hook.c                        | 100 +++++++++++++++++++++++++++-------\n hook.h                        |  10 ++++\n setup.c                       |   1 +\n t/helper/test-path-utils.c    |  10 ----\n t/t0060-path-utils.sh         |  41 --------------\n t/t1350-config-hooks-path.sh  |   4 ++\n t/t1800-hook.sh               |  40 ++++++++++----\n t/t5601-clone.sh              |  19 +++++++\n 14 files changed, 158 insertions(+), 167 deletions(-)\n\n\nbase-commit: 47b6d90e91835082010da926f6a844d4441c57a6\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-1732%2Fdscho%2Fvarious-fixes-for-v2.45.1-and-friends-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1732/dscho/various-fixes-for-v2.45.1-and-friends-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/1732\n-- \ngitgitgadget\n"},{"id":"495003","messageId":"d4a003bf2ceafcc6d47d01d21b7faff48c9e85aa.1715987756.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.git.1715987756.gitgitgadget@gmail.com","subject":"[PATCH 1/8] hook: plug a new memory leak","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-17T23:15:49Z","receivedAt":"2024-05-17T23:16:01Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nIn 8db1e8743c0 (clone: prevent hooks from running during a clone,\n2024-03-28), I introduced an inadvertent memory leak that was\nunfortunately not caught before v2.45.1 was released. Here is a fix.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n hook.c | 4 +++-\n 1 file changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/hook.c b/hook.c\nindex 632b537b993..fc974cee1d8 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -18,8 +18,10 @@ static int identical_to_template_hook(const char *name, const char *path)\n \t\tfound_template_hook = access(template_path.buf, X_OK) >= 0;\n \t}\n #endif\n-\tif (!found_template_hook)\n+\tif (!found_template_hook) {\n+\t\tstrbuf_release(&template_path);\n \t\treturn 0;\n+\t}\n \n \tret = do_files_match(template_path.buf, path);\n \n-- \ngitgitgadget\n\n"},{"id":"495004","messageId":"961dfc35f426388d660cca4e92f43e169819886a.1715987756.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.git.1715987756.gitgitgadget@gmail.com","subject":"[PATCH 2/8] init: use the correct path of the templates directory again","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-17T23:15:50Z","receivedAt":"2024-05-17T23:16:02Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nIn df93e407f06 (init: refactor the template directory discovery into its\nown function, 2024-03-29), I refactored the way the templates directory\nis discovered.\n\nThe refactoring was faithful, but missed a reference in the `Makefile`\nwhere the `DEFAULT_GIT_TEMPLATE_DIR` constant is defined. As a\nconsequence, Git v2.45.1 and friends will always use the hard-coded path\n`/usr/share/git-core/templates`.\n\nLet's fix that by defining the `DEFAULT_GIT_TEMPLATE_DIR` when building\n`setup.o`, where that constant is actually used.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n Makefile | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/Makefile b/Makefile\nindex 093829ae283..4b1502ba2c6 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2751,7 +2751,7 @@ exec-cmd.sp exec-cmd.s exec-cmd.o: EXTRA_CPPFLAGS = \\\n \t'-DFALLBACK_RUNTIME_PREFIX=\"$(prefix_SQ)\"'\n \n builtin/init-db.sp builtin/init-db.s builtin/init-db.o: GIT-PREFIX\n-builtin/init-db.sp builtin/init-db.s builtin/init-db.o: EXTRA_CPPFLAGS = \\\n+setup.sp setup.s setup.o: EXTRA_CPPFLAGS = \\\n \t-DDEFAULT_GIT_TEMPLATE_DIR='\"$(template_dir_SQ)\"'\n \n config.sp config.s config.o: GIT-PREFIX\n-- \ngitgitgadget\n\n"},{"id":"495005","messageId":"57db89a14977bdff01f8f82cb4d6f85cc49d4b55.1715987756.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.git.1715987756.gitgitgadget@gmail.com","subject":"[PATCH 3/8] Revert \"core.hooksPath: add some protection while cloning\"","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-17T23:15:51Z","receivedAt":"2024-05-17T23:16:03Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nThis defense-in-depth was intended to protect the clone operation\nagainst future escalations where bugs in `git clone` would allow\nattackers to write arbitrary files in the `.git/` directory would allow\nfor Remote Code Execution attacks via maliciously-placed hooks.\n\nHowever, it turns out that the `core.hooksPath` protection has\nunintentional side effects so severe that they do not justify the\nbenefit of the protections. For example, it has been reported in\nhttps://lore.kernel.org/git/FAFA34CB-9732-4A0A-87FB-BDB272E6AEE8@alchemists.io/\nthat the following invocation, which is intended to make `git clone`\nsafer, is itself broken by that protective measure:\n\n\tgit clone --config core.hooksPath=/dev/null <url>\n\nSince it turns out that the benefit does not justify the cost, let's revert\n20f3588efc6 (core.hooksPath: add some protection while cloning,\n2024-03-30).\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n config.c        | 13 +------------\n t/t1800-hook.sh | 15 ---------------\n 2 files changed, 1 insertion(+), 27 deletions(-)\n\ndiff --git a/config.c b/config.c\nindex 85b37f2ee09..8c1c4071f0d 100644\n--- a/config.c\n+++ b/config.c\n@@ -1525,19 +1525,8 @@ static int git_default_core_config(const char *var, const char *value, void *cb)\n \tif (!strcmp(var, \"core.attributesfile\"))\n \t\treturn git_config_pathname(&git_attributes_file, var, value);\n \n-\tif (!strcmp(var, \"core.hookspath\")) {\n-\t\tif (current_config_scope() == CONFIG_SCOPE_LOCAL &&\n-\t\t    git_env_bool(\"GIT_CLONE_PROTECTION_ACTIVE\", 0))\n-\t\t\tdie(_(\"active `core.hooksPath` found in the local \"\n-\t\t\t      \"repository config:\\n\\t%s\\nFor security \"\n-\t\t\t      \"reasons, this is disallowed by default.\\nIf \"\n-\t\t\t      \"this is intentional and the hook should \"\n-\t\t\t      \"actually be run, please\\nrun the command \"\n-\t\t\t      \"again with \"\n-\t\t\t      \"`GIT_CLONE_PROTECTION_ACTIVE=false`\"),\n-\t\t\t    value);\n+\tif (!strcmp(var, \"core.hookspath\"))\n \t\treturn git_config_pathname(&git_hooks_path, var, value);\n-\t}\n \n \tif (!strcmp(var, \"core.bare\")) {\n \t\tis_bare_repository_cfg = git_config_bool(var, value);\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 7ee12e6f48a..2ef3579fa7c 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -177,19 +177,4 @@ test_expect_success 'git hook run a hook with a bad shebang' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'clone protections' '\n-\ttest_config core.hooksPath \"$(pwd)/my-hooks\" &&\n-\tmkdir -p my-hooks &&\n-\twrite_script my-hooks/test-hook <<-\\EOF &&\n-\techo Hook ran $1\n-\tEOF\n-\n-\tgit hook run test-hook 2>err &&\n-\tgrep \"Hook ran\" err &&\n-\ttest_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \\\n-\t\tgit hook run test-hook 2>err &&\n-\tgrep \"active .core.hooksPath\" err &&\n-\t! grep \"Hook ran\" err\n-'\n-\n test_done\n-- \ngitgitgadget\n\n"},{"id":"495006","messageId":"7d5ef6db2a9c3c7a1b0ba78873d4202403768769.1715987756.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.git.1715987756.gitgitgadget@gmail.com","subject":"[PATCH 4/8] tests: verify that `clone -c core.hooksPath=/dev/null` works again","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-17T23:15:52Z","receivedAt":"2024-05-17T23:16:04Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nAs part of the protections added in Git v2.45.1 and friends,\nrepository-local `core.hooksPath` settings are no longer allowed, as a\ndefense-in-depth mechanism to prevent future Git vulnerabilities to\nraise to critical level if those vulnerabilities inadvertently allow the\nrepository-local config to be written.\n\nWhat the added protection did not anticipate is that such a\nrepository-local `core.hooksPath` can not only be used to point to\nmaliciously-placed scripts in the current worktree, but also to\n_prevent_ hooks from being called altogether.\n\nWe just reverted the `core.hooksPath` protections, based on the Git\nmaintainer's recommendation in\nhttps://lore.kernel.org/git/xmqq4jaxvm8z.fsf@gitster.g/ to address this\nconcern as well as related ones. Let's make sure that we won't regress\nwhile trying to protect the clone operation further.\n\nReported-by: Brooke Kuhlmann <brooke@alchemists.io>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n t/t1350-config-hooks-path.sh | 4 ++++\n 1 file changed, 4 insertions(+)\n\ndiff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh\nindex f6dc83e2aab..1eae346a6e3 100755\n--- a/t/t1350-config-hooks-path.sh\n+++ b/t/t1350-config-hooks-path.sh\n@@ -41,4 +41,8 @@ test_expect_success 'git rev-parse --git-path hooks' '\n \ttest .git/custom-hooks/abc = \"$(cat actual)\"\n '\n \n+test_expect_success 'core.hooksPath=/dev/null' '\n+\tgit clone -c core.hooksPath=/dev/null . no-templates\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"495007","messageId":"a4f5eeef6677462267d1dbf4b2b58d30aa013684.1715987756.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.git.1715987756.gitgitgadget@gmail.com","subject":"[PATCH 5/8] hook(clone protections): add escape hatch","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-17T23:15:53Z","receivedAt":"2024-05-17T23:16:05Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nAs defense-in-depth measures, v2.39.4 and friends leading up to v2.45.1\nintroduced code that detects when hooks have been installed during a\n`git clone`, which is indicative of a common attack vector with critical\nseverity that allows Remote Code Execution.\n\nThere are legitimate use cases for such behavior, though, for example\nwhen those hooks stem from Git's own templates, which system\nadministrators are at liberty to modify to enforce, say, commit message\nconventions. The git clone protections specifically add exceptions to\nallow for that.\n\nAnother legitimate use case that has been identified too late to be\nhandled in these security bug-fix versions is Git LFS: It behaves\nsomewhat similar to common attack vectors by writing a few hooks while\nrunning the `smudge` filter during a regular clone, which means that Git\nhas no chance to know that the hooks are benign and e.g. the\n`post-checkout` hook can be safely executed as part of the clone\noperation.\n\nTo help Git LFS, and other tools behaving similarly (if there are any),\nlet's add a new, multi-valued `safe.hook.sha256` config setting. Like\nthe already-existing `safe.*` settings, it is ignored in\nrepository-local configs, and it is interpreted as a list of SHA-256\nchecksums of hooks' contents that are safe to execute during a clone\noperation. Future Git LFS versions will need to write those entries at\nthe same time they install the `smudge`/`clean` filters.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n Documentation/config/safe.txt |  6 ++++\n hook.c                        | 66 ++++++++++++++++++++++++++++++++---\n t/t1800-hook.sh               | 15 ++++++++\n 3 files changed, 82 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/config/safe.txt b/Documentation/config/safe.txt\nindex bde7f31459b..69ee845be89 100644\n--- a/Documentation/config/safe.txt\n+++ b/Documentation/config/safe.txt\n@@ -59,3 +59,9 @@ which id the original user has.\n If that is not what you would prefer and want git to only trust\n repositories that are owned by root instead, then you can remove\n the `SUDO_UID` variable from root's environment before invoking git.\n+\n+safe.hook.sha256::\n+\tThe value is the SHA-256 of hooks that are considered to be safe\n+\tto run during a clone operation.\n++\n+Multiple values can be added via `git config --global --add`.\ndiff --git a/hook.c b/hook.c\nindex fc974cee1d8..a2479738451 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -2,6 +2,7 @@\n #include \"hook.h\"\n #include \"run-command.h\"\n #include \"config.h\"\n+#include \"strmap.h\"\n \n static int identical_to_template_hook(const char *name, const char *path)\n {\n@@ -29,11 +30,65 @@ static int identical_to_template_hook(const char *name, const char *path)\n \treturn ret;\n }\n \n+static struct strset safe_hook_sha256s = STRSET_INIT;\n+static int safe_hook_sha256s_initialized;\n+\n+static int get_sha256_of_file_contents(const char *path, char *sha256)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tint fd;\n+\tssize_t res;\n+\n+\tgit_hash_ctx ctx;\n+\tconst struct git_hash_algo *algo = &hash_algos[GIT_HASH_SHA256];\n+\tunsigned char hash[GIT_MAX_RAWSZ];\n+\n+\tif ((fd = open(path, O_RDONLY)) < 0)\n+\t\treturn -1;\n+\tres = strbuf_read(&sb, fd, 400);\n+\tclose(fd);\n+\tif (res < 0)\n+\t\treturn -1;\n+\n+\talgo->init_fn(&ctx);\n+\talgo->update_fn(&ctx, sb.buf, sb.len);\n+\tstrbuf_release(&sb);\n+\talgo->final_fn(hash, &ctx);\n+\n+\thash_to_hex_algop_r(sha256, hash, algo);\n+\n+\treturn 0;\n+}\n+\n+static int safe_hook_cb(const char *key, const char *value, void *d)\n+{\n+\tstruct strset *set = d;\n+\n+\tif (value && !strcmp(key, \"safe.hook.sha256\"))\n+\t\tstrset_add(set, value);\n+\n+\treturn 0;\n+}\n+\n+static int is_hook_safe_during_clone(const char *name, const char *path, char *sha256)\n+{\n+\tif (get_sha256_of_file_contents(path, sha256) < 0)\n+\t\treturn 0;\n+\n+\tif (!safe_hook_sha256s_initialized) {\n+\t\tsafe_hook_sha256s_initialized = 1;\n+\t\tgit_protected_config(safe_hook_cb, &safe_hook_sha256s);\n+\t}\n+\n+\treturn strset_contains(&safe_hook_sha256s, sha256);\n+}\n+\n const char *find_hook(const char *name)\n {\n \tstatic struct strbuf path = STRBUF_INIT;\n \n \tint found_hook;\n+\tchar sha256[GIT_SHA256_HEXSZ + 1] = { '\\0' };\n \n \tstrbuf_reset(&path);\n \tstrbuf_git_path(&path, \"hooks/%s\", name);\n@@ -65,13 +120,14 @@ const char *find_hook(const char *name)\n \t\treturn NULL;\n \t}\n \tif (!git_hooks_path && git_env_bool(\"GIT_CLONE_PROTECTION_ACTIVE\", 0) &&\n-\t    !identical_to_template_hook(name, path.buf))\n+\t    !identical_to_template_hook(name, path.buf) &&\n+\t    !is_hook_safe_during_clone(name, path.buf, sha256))\n \t\tdie(_(\"active `%s` hook found during `git clone`:\\n\\t%s\\n\"\n \t\t      \"For security reasons, this is disallowed by default.\\n\"\n-\t\t      \"If this is intentional and the hook should actually \"\n-\t\t      \"be run, please\\nrun the command again with \"\n-\t\t      \"`GIT_CLONE_PROTECTION_ACTIVE=false`\"),\n-\t\t    name, path.buf);\n+\t\t      \"If this is intentional and the hook is safe to run, \"\n+\t\t      \"please run the following command and try again:\\n\\n\"\n+\t\t      \"  git config --global --add safe.hook.sha256 %s\"),\n+\t\t    name, path.buf, sha256);\n \treturn path.buf;\n }\n \ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 2ef3579fa7c..0f74c9154d0 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -177,4 +177,19 @@ test_expect_success 'git hook run a hook with a bad shebang' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success '`safe.hook.sha256` and clone protections' '\n+\tgit init safe-hook &&\n+\twrite_script safe-hook/.git/hooks/pre-push <<-\\EOF &&\n+\techo \"called hook\" >safe-hook.log\n+\tEOF\n+\n+\ttest_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \\\n+\t\tgit -C safe-hook hook run pre-push 2>err &&\n+\tcmd=\"$(grep \"git config --global --add safe.hook.sha256 [0-9a-f]\" err)\" &&\n+\teval \"$cmd\" &&\n+\tGIT_CLONE_PROTECTION_ACTIVE=true \\\n+\t\tgit -C safe-hook hook run pre-push &&\n+\ttest \"called hook\" = \"$(cat safe-hook/safe-hook.log)\"\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"495008","messageId":"98465797e72cf039ace4138ab1e03e4fc7465ea2.1715987756.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.git.1715987756.gitgitgadget@gmail.com","subject":"[PATCH 6/8] hooks(clone protections): special-case current Git LFS hooks","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-17T23:15:54Z","receivedAt":"2024-05-17T23:16:06Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nA notable regression in v2.45.1 and friends (all the way down to\nv2.39.4) has been that Git LFS-enabled clones error out with a message\nindicating that the `post-checkout` hook has been tampered with while\ncloning, and as a safety measure it is not executed.\n\nA generic fix for benign third-party applications wishing to write hooks\nduring clone operations has been implemented in the parent of this\ncommit: said applications are expected to add `safe.hook.sha256` values\nto a protected config.\n\nHowever, the current version of Git LFS, v3.5.1, cannot be adapted\nretroactively; Therefore, let's just hard-code the SHA-256 values for\nthis version. That way, Git LFS usage will no longer be broken, and the\nnext Git LFS version can be taught to add those `safe.hook.sha256`\nentries.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n hook.c          | 11 +++++++++++\n t/t1800-hook.sh | 20 ++++++++++++++++++++\n 2 files changed, 31 insertions(+)\n\ndiff --git a/hook.c b/hook.c\nindex a2479738451..f810ee133be 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -75,6 +75,17 @@ static int is_hook_safe_during_clone(const char *name, const char *path, char *s\n \tif (get_sha256_of_file_contents(path, sha256) < 0)\n \t\treturn 0;\n \n+\t/* Hard-code known-safe values for Git LFS v3.4.0..v3.5.1 */\n+\tif ((!strcmp(\"pre-push\", name) &&\n+\t     !strcmp(sha256, \"df5417b2daa3aa144c19681d1e997df7ebfe144fb7e3e05138bd80ae998008e4\")) ||\n+\t    (!strcmp(\"post-checkout\", name) &&\n+\t     !strcmp(sha256, \"791471b4ff472aab844a4fceaa48bbb0a12193616f971e8e940625498b4938a6\")) ||\n+\t    (!strcmp(\"post-commit\", name) &&\n+\t     !strcmp(sha256, \"21e961572bb3f43a5f2fbafc1cc764d86046cc2e5f0bbecebfe9684a0b73b664\")) ||\n+\t    (!strcmp(\"post-merge\", name) &&\n+\t     !strcmp(sha256, \"75da0da66a803b4b030ad50801ba57062c6196105eb1d2251590d100edb9390b\")))\n+\t\treturn 1;\n+\n \tif (!safe_hook_sha256s_initialized) {\n \t\tsafe_hook_sha256s_initialized = 1;\n \t\tgit_protected_config(safe_hook_cb, &safe_hook_sha256s);\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 0f74c9154d0..af66999aff3 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -192,4 +192,24 @@ test_expect_success '`safe.hook.sha256` and clone protections' '\n \ttest \"called hook\" = \"$(cat safe-hook/safe-hook.log)\"\n '\n \n+write_lfs_pre_push_hook () {\n+\twrite_script \"$1\" <<-\\EOF\n+\tcommand -v git-lfs >/dev/null 2>&1 || { echo >&2 \"\\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\\n\"; exit 2; }\n+\tgit lfs pre-push \"$@\"\n+\tEOF\n+}\n+\n+test_expect_success 'Git LFS special-handling in clone protections' '\n+\tgit init lfs-hooks &&\n+\twrite_lfs_pre_push_hook lfs-hooks/.git/hooks/pre-push &&\n+\twrite_script git-lfs <<-\\EOF &&\n+\techo \"called $*\" >fake-git-lfs.log\n+\tEOF\n+\n+\tPATH=\"$PWD:$PATH\" GIT_CLONE_PROTECTION_ACTIVE=true \\\n+\t\tgit -C lfs-hooks hook run pre-push &&\n+\ttest_write_lines \"called pre-push\" >expect &&\n+\ttest_cmp lfs-hooks/fake-git-lfs.log expect\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"495009","messageId":"c487bd06be813f7442494fb124ccb2c48d327dcd.1715987756.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.git.1715987756.gitgitgadget@gmail.com","subject":"[PATCH 7/8] hooks(clone protections): simplify templates hooks validation","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-17T23:15:55Z","receivedAt":"2024-05-17T23:16:07Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nWhen an active hook is encountered during a clone operation, to protect\nagainst Remote Code Execution attack vectors, Git checks whether the\nhook was copied over from the templates directory.\n\nWhen that logic was introduced, there was no other way to check this\nthan to add a function to compare files.\n\nIn the meantime, we've added code to compute the SHA-256 checksum of a\ngiven hook and compare that checksum against a list of known-safe ones.\n\nLet's simplify the logic by adding to said list when copying the\ntemplates' hooks.\n\nWe need to be careful to support multi-process operations such as\nrecursive submodule clones: In such a scenario, the list of SHA-256\nchecksums that is kept in memory is not enough, we also have to pass the\ninformation down to child processes via `GIT_CONFIG_PARAMETERS`.\n\nExtend the regression test in t5601 to ensure that recursive clones are\nhandled as expected.\n\nNote: Technically there is no way that the checksums computed while\ninitializing the submodules' gitdirs can be passed to the process that\nperforms the checkout: For historical reasons, these operations are\nperformed in processes spawned in separate loops from the\nsuper-project's `git clone` process. But since the templates from which\nthe submodules are initialized are the very same as the ones from which\nthe super-project is initialized, we can get away with using the list of\nSHA-256 checksums that is computed when initializing the super-project\nand passing that down to the `submodule--helper` processes that perform\nthe recursive checkout.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n builtin/init-db.c |  7 +++++++\n hook.c            | 43 ++++++++++++++++---------------------------\n hook.h            | 10 ++++++++++\n setup.c           |  1 +\n t/t5601-clone.sh  | 19 +++++++++++++++++++\n 5 files changed, 53 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/init-db.c b/builtin/init-db.c\nindex a101e7f94c1..64357fdada4 100644\n--- a/builtin/init-db.c\n+++ b/builtin/init-db.c\n@@ -10,6 +10,8 @@\n #include \"exec-cmd.h\"\n #include \"parse-options.h\"\n #include \"worktree.h\"\n+#include \"run-command.h\"\n+#include \"hook.h\"\n \n #ifdef NO_TRUSTABLE_FILEMODE\n #define TEST_FILEMODE 0\n@@ -28,6 +30,7 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,\n \tsize_t path_baselen = path->len;\n \tsize_t template_baselen = template_path->len;\n \tstruct dirent *de;\n+\tint is_hooks_dir = ends_with(template_path->buf, \"/hooks/\");\n \n \t/* Note: if \".git/hooks\" file exists in the repository being\n \t * re-initialized, /etc/core-git/templates/hooks/update would\n@@ -80,6 +83,10 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,\n \t\t\tstrbuf_release(&lnk);\n \t\t}\n \t\telse if (S_ISREG(st_template.st_mode)) {\n+\t\t\tif (is_hooks_dir &&\n+\t\t\t    is_executable(template_path->buf))\n+\t\t\t\tadd_safe_hook(template_path->buf);\n+\n \t\t\tif (copy_file(path->buf, template_path->buf, st_template.st_mode))\n \t\t\t\tdie_errno(_(\"cannot copy '%s' to '%s'\"),\n \t\t\t\t\t  template_path->buf, path->buf);\ndiff --git a/hook.c b/hook.c\nindex f810ee133be..b69cc691bdf 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -4,32 +4,6 @@\n #include \"config.h\"\n #include \"strmap.h\"\n \n-static int identical_to_template_hook(const char *name, const char *path)\n-{\n-\tconst char *env = getenv(\"GIT_CLONE_TEMPLATE_DIR\");\n-\tconst char *template_dir = get_template_dir(env && *env ? env : NULL);\n-\tstruct strbuf template_path = STRBUF_INIT;\n-\tint found_template_hook, ret;\n-\n-\tstrbuf_addf(&template_path, \"%s/hooks/%s\", template_dir, name);\n-\tfound_template_hook = access(template_path.buf, X_OK) >= 0;\n-#ifdef STRIP_EXTENSION\n-\tif (!found_template_hook) {\n-\t\tstrbuf_addstr(&template_path, STRIP_EXTENSION);\n-\t\tfound_template_hook = access(template_path.buf, X_OK) >= 0;\n-\t}\n-#endif\n-\tif (!found_template_hook) {\n-\t\tstrbuf_release(&template_path);\n-\t\treturn 0;\n-\t}\n-\n-\tret = do_files_match(template_path.buf, path);\n-\n-\tstrbuf_release(&template_path);\n-\treturn ret;\n-}\n-\n static struct strset safe_hook_sha256s = STRSET_INIT;\n static int safe_hook_sha256s_initialized;\n \n@@ -60,6 +34,22 @@ static int get_sha256_of_file_contents(const char *path, char *sha256)\n \treturn 0;\n }\n \n+void add_safe_hook(const char *path)\n+{\n+\tchar sha256[GIT_SHA256_HEXSZ + 1] = { '\\0' };\n+\n+\tif (!get_sha256_of_file_contents(path, sha256)) {\n+\t\tchar *p;\n+\n+\t\tstrset_add(&safe_hook_sha256s, sha256);\n+\n+\t\t/* support multi-process operations e.g. recursive clones */\n+\t\tp = xstrfmt(\"safe.hook.sha256=%s\", sha256);\n+\t\tgit_config_push_parameter(p);\n+\t\tfree(p);\n+\t}\n+}\n+\n static int safe_hook_cb(const char *key, const char *value, void *d)\n {\n \tstruct strset *set = d;\n@@ -131,7 +121,6 @@ const char *find_hook(const char *name)\n \t\treturn NULL;\n \t}\n \tif (!git_hooks_path && git_env_bool(\"GIT_CLONE_PROTECTION_ACTIVE\", 0) &&\n-\t    !identical_to_template_hook(name, path.buf) &&\n \t    !is_hook_safe_during_clone(name, path.buf, sha256))\n \t\tdie(_(\"active `%s` hook found during `git clone`:\\n\\t%s\\n\"\n \t\t      \"For security reasons, this is disallowed by default.\\n\"\ndiff --git a/hook.h b/hook.h\nindex 4258b13da0d..e2034ee8b23 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -82,4 +82,14 @@ int run_hooks(const char *hook_name);\n  * hook. This function behaves like the old run_hook_le() API.\n  */\n int run_hooks_l(const char *hook_name, ...);\n+\n+/**\n+ * Mark the contents of the provided path as safe to run during a clone\n+ * operation.\n+ *\n+ * This function is mainly used when copying templates to mark the\n+ * just-copied hooks as benign.\n+ */\n+void add_safe_hook(const char *path);\n+\n #endif\ndiff --git a/setup.c b/setup.c\nindex c3301f5ab82..7f7538c9bf7 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -7,6 +7,7 @@\n #include \"promisor-remote.h\"\n #include \"quote.h\"\n #include \"exec-cmd.h\"\n+#include \"hook.h\"\n \n static int inside_git_dir = -1;\n static int inside_work_tree = -1;\ndiff --git a/t/t5601-clone.sh b/t/t5601-clone.sh\nindex 20deca0231b..71eaa3d1e14 100755\n--- a/t/t5601-clone.sh\n+++ b/t/t5601-clone.sh\n@@ -819,6 +819,25 @@ test_expect_success 'clone with init.templatedir runs hooks' '\n \t\tgit config --unset init.templateDir &&\n \t\t! grep \"active .* hook found\" err &&\n \t\ttest_path_is_missing hook-run-local-config/hook.run\n+\t) &&\n+\n+\ttest_config_global protocol.file.allow always &&\n+\tgit -C tmpl/hooks submodule add \"$(pwd)/tmpl/hooks\" sub &&\n+\ttest_tick &&\n+\tgit -C tmpl/hooks add .gitmodules sub &&\n+\tgit -C tmpl/hooks commit -m submodule &&\n+\n+\t(\n+\t\tsane_unset GIT_TEMPLATE_DIR &&\n+\t\tNO_SET_GIT_TEMPLATE_DIR=t &&\n+\t\texport NO_SET_GIT_TEMPLATE_DIR &&\n+\n+\t\tgit -c init.templateDir=\"$(pwd)/tmpl\" \\\n+\t\t\tclone --recurse-submodules \\\n+\t\t\ttmpl/hooks hook-run-submodule 2>err &&\n+\t\t! grep \"active .* hook found\" err &&\n+\t\ttest_path_is_file hook-run-submodule/hook.run &&\n+\t\ttest_path_is_file hook-run-submodule/sub/hook.run\n \t)\n '\n \n-- \ngitgitgadget\n\n"},{"id":"495010","messageId":"c45c33d8e3f658c7cb4bd33000bb507333727468.1715987756.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.git.1715987756.gitgitgadget@gmail.com","subject":"[PATCH 8/8] Revert \"Add a helper function to compare file contents\"","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-17T23:15:56Z","receivedAt":"2024-05-17T23:16:08Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nNow that during a `git clone`, the hooks' contents are no longer\ncompared to the templates' files', the caller for which the\n`do_files_match()` function was introduced is gone, and therefore this\nfunction can be retired, too.\n\nThis reverts commit 584de0b4c23 (Add a helper function to compare file\ncontents, 2024-03-30).\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n cache.h                    | 14 ---------\n copy.c                     | 58 --------------------------------------\n t/helper/test-path-utils.c | 10 -------\n t/t0060-path-utils.sh      | 41 ---------------------------\n 4 files changed, 123 deletions(-)\n\ndiff --git a/cache.h b/cache.h\nindex 16b34799bfd..8c5fb1e1ba1 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1785,20 +1785,6 @@ int copy_fd(int ifd, int ofd);\n int copy_file(const char *dst, const char *src, int mode);\n int copy_file_with_time(const char *dst, const char *src, int mode);\n \n-/*\n- * Compare the file mode and contents of two given files.\n- *\n- * If both files are actually symbolic links, the function returns 1 if the link\n- * targets are identical or 0 if they are not.\n- *\n- * If any of the two files cannot be accessed or in case of read failures, this\n- * function returns 0.\n- *\n- * If the file modes and contents are identical, the function returns 1,\n- * otherwise it returns 0.\n- */\n-int do_files_match(const char *path1, const char *path2);\n-\n void write_or_die(int fd, const void *buf, size_t count);\n void fsync_or_die(int fd, const char *);\n int fsync_component(enum fsync_component component, int fd);\ndiff --git a/copy.c b/copy.c\nindex 8492f6fc831..4de6a110f09 100644\n--- a/copy.c\n+++ b/copy.c\n@@ -65,61 +65,3 @@ int copy_file_with_time(const char *dst, const char *src, int mode)\n \t\treturn copy_times(dst, src);\n \treturn status;\n }\n-\n-static int do_symlinks_match(const char *path1, const char *path2)\n-{\n-\tstruct strbuf buf1 = STRBUF_INIT, buf2 = STRBUF_INIT;\n-\tint ret = 0;\n-\n-\tif (!strbuf_readlink(&buf1, path1, 0) &&\n-\t    !strbuf_readlink(&buf2, path2, 0))\n-\t\tret = !strcmp(buf1.buf, buf2.buf);\n-\n-\tstrbuf_release(&buf1);\n-\tstrbuf_release(&buf2);\n-\treturn ret;\n-}\n-\n-int do_files_match(const char *path1, const char *path2)\n-{\n-\tstruct stat st1, st2;\n-\tint fd1 = -1, fd2 = -1, ret = 1;\n-\tchar buf1[8192], buf2[8192];\n-\n-\tif ((fd1 = open_nofollow(path1, O_RDONLY)) < 0 ||\n-\t    fstat(fd1, &st1) || !S_ISREG(st1.st_mode)) {\n-\t\tif (fd1 < 0 && errno == ELOOP)\n-\t\t\t/* maybe this is a symbolic link? */\n-\t\t\treturn do_symlinks_match(path1, path2);\n-\t\tret = 0;\n-\t} else if ((fd2 = open_nofollow(path2, O_RDONLY)) < 0 ||\n-\t\t   fstat(fd2, &st2) || !S_ISREG(st2.st_mode)) {\n-\t\tret = 0;\n-\t}\n-\n-\tif (ret)\n-\t\t/* to match, neither must be executable, or both */\n-\t\tret = !(st1.st_mode & 0111) == !(st2.st_mode & 0111);\n-\n-\tif (ret)\n-\t\tret = st1.st_size == st2.st_size;\n-\n-\twhile (ret) {\n-\t\tssize_t len1 = read_in_full(fd1, buf1, sizeof(buf1));\n-\t\tssize_t len2 = read_in_full(fd2, buf2, sizeof(buf2));\n-\n-\t\tif (len1 < 0 || len2 < 0 || len1 != len2)\n-\t\t\tret = 0; /* read error or different file size */\n-\t\telse if (!len1) /* len2 is also 0; hit EOF on both */\n-\t\t\tbreak; /* ret is still true */\n-\t\telse\n-\t\t\tret = !memcmp(buf1, buf2, len1);\n-\t}\n-\n-\tif (fd1 >= 0)\n-\t\tclose(fd1);\n-\tif (fd2 >= 0)\n-\t\tclose(fd2);\n-\n-\treturn ret;\n-}\ndiff --git a/t/helper/test-path-utils.c b/t/helper/test-path-utils.c\nindex 0e0de218076..f69709d674f 100644\n--- a/t/helper/test-path-utils.c\n+++ b/t/helper/test-path-utils.c\n@@ -495,16 +495,6 @@ int cmd__path_utils(int argc, const char **argv)\n \t\treturn !!res;\n \t}\n \n-\tif (argc == 4 && !strcmp(argv[1], \"do_files_match\")) {\n-\t\tint ret = do_files_match(argv[2], argv[3]);\n-\n-\t\tif (ret)\n-\t\t\tprintf(\"equal\\n\");\n-\t\telse\n-\t\t\tprintf(\"different\\n\");\n-\t\treturn !ret;\n-\t}\n-\n \tfprintf(stderr, \"%s: unknown function name: %s\\n\", argv[0],\n \t\targv[1] ? argv[1] : \"(there was none)\");\n \treturn 1;\ndiff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh\nindex 73d0e1a7f10..68e29c904a6 100755\n--- a/t/t0060-path-utils.sh\n+++ b/t/t0060-path-utils.sh\n@@ -560,45 +560,4 @@ test_expect_success !VALGRIND,RUNTIME_PREFIX,CAN_EXEC_IN_PWD '%(prefix)/ works'\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'do_files_match()' '\n-\ttest_seq 0 10 >0-10.txt &&\n-\ttest_seq -1 10 >-1-10.txt &&\n-\ttest_seq 1 10 >1-10.txt &&\n-\ttest_seq 1 9 >1-9.txt &&\n-\ttest_seq 0 8 >0-8.txt &&\n-\n-\ttest-tool path-utils do_files_match 0-10.txt 0-10.txt >out &&\n-\n-\tassert_fails() {\n-\t\ttest_must_fail \\\n-\t\ttest-tool path-utils do_files_match \"$1\" \"$2\" >out &&\n-\t\tgrep different out\n-\t} &&\n-\n-\tassert_fails 0-8.txt 1-9.txt &&\n-\tassert_fails -1-10.txt 0-10.txt &&\n-\tassert_fails 1-10.txt 1-9.txt &&\n-\tassert_fails 1-10.txt .git &&\n-\tassert_fails does-not-exist 1-10.txt &&\n-\n-\tif test_have_prereq FILEMODE\n-\tthen\n-\t\tcp 0-10.txt 0-10.x &&\n-\t\tchmod a+x 0-10.x &&\n-\t\tassert_fails 0-10.txt 0-10.x\n-\tfi &&\n-\n-\tif test_have_prereq SYMLINKS\n-\tthen\n-\t\tln -sf 0-10.txt symlink &&\n-\t\tln -s 0-10.txt another-symlink &&\n-\t\tln -s over-the-ocean yet-another-symlink &&\n-\t\tln -s \"$PWD/0-10.txt\" absolute-symlink &&\n-\t\tassert_fails 0-10.txt symlink &&\n-\t\ttest-tool path-utils do_files_match symlink another-symlink &&\n-\t\tassert_fails symlink yet-another-symlink &&\n-\t\tassert_fails symlink absolute-symlink\n-\tfi\n-'\n-\n test_done\n-- \ngitgitgadget\n"},{"id":"495014","messageId":"xmqqa5kodmil.fsf@gitster.g","threadId":"61509","inReplyTo":"pull.1732.git.1715987756.gitgitgadget@gmail.com","subject":"Re: [PATCH 0/8] Various fixes for v2.45.1 and friends","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-17T23:52:34Z","receivedAt":"2024-05-17T23:52:37Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Johannes Schindelin via GitGitGadget\" <gitgitgadget@gmail.com>\nwrites:\n\n> There have been a couple of issues that were reported about v2.45.1, and in\n> addition I have noticed some myself:\n>\n>  * a memory leak in the clone protection logic\n>  * a missed adjustment in the Makefile that leads to an incorrect templates\n>    path in v2.39.4, v2.40.2 and v2.41.1 (but not in v2.42.2, ..., v2.45.1)\n>  * an overzealous core.hooksPath check\n>  * that Git LFS clone problem where it exits with an error (even if the\n>    clone often succeeded...)\n\nI thought brian expressed an interest in the issues these patches\nattempt to address, so I thought it is helpful to Cc them to his\nattention.\n\n> This patch series is based on maint-2.39 to allow for (relatively) easy\n> follow-up versions v2.39.5, ..., v2.45.2.\n\nThanks for basing them on the oldest version that is relevant.\n\n"},{"id":"495015","messageId":"3b140a63-ddb1-bd84-94c5-51e39d60c011@gmx.de","threadId":"61509","inReplyTo":"xmqqa5kodmil.fsf@gitster.g","subject":"Re: [PATCH 0/8] Various fixes for v2.45.1 and friends","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2024-05-18T00:02:21Z","receivedAt":"2024-05-18T00:02:34Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Junio and brian,\n\nOn Fri, 17 May 2024, Junio C Hamano wrote:\n\n> \"Johannes Schindelin via GitGitGadget\" <gitgitgadget@gmail.com>\n> writes:\n>\n> > There have been a couple of issues that were reported about v2.45.1, and in\n> > addition I have noticed some myself:\n> >\n> >  * a memory leak in the clone protection logic\n> >  * a missed adjustment in the Makefile that leads to an incorrect templates\n> >    path in v2.39.4, v2.40.2 and v2.41.1 (but not in v2.42.2, ..., v2.45.1)\n> >  * an overzealous core.hooksPath check\n> >  * that Git LFS clone problem where it exits with an error (even if the\n> >    clone often succeeded...)\n>\n> I thought brian expressed an interest in the issues these patches\n> attempt to address, so I thought it is helpful to Cc them to his\n> attention.\n\noh my. I forgot to add the Cc (I guess I should have slept first, but I\nwanted to get this out so that the Git LFS breakage as well as the other\nbreakages could be addressed as swiftly as possible)!\n\nI really meant to Cc: you, brian, hoping for your substantial expertise\nand kind code reviews to help get this landed soonish, in a shape that\nmakes Git better, and hopefully averts more breakages.\n\n> > This patch series is based on maint-2.39 to allow for (relatively) easy\n> > follow-up versions v2.39.5, ..., v2.45.2.\n>\n> Thanks for basing them on the oldest version that is relevant.\n\nI also meant to provide `maint-*` branches but basically ran out of time.\nWill work on that next week. I expect quite a few merge conflicts.\n\nCiao,\nJohannes\n"},{"id":"495016","messageId":"xmqq4jawdlpa.fsf@gitster.g","threadId":"61509","inReplyTo":"7d5ef6db2a9c3c7a1b0ba78873d4202403768769.1715987756.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 4/8] tests: verify that `clone -c core.hooksPath=/dev/null` works again","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-18T00:10:09Z","receivedAt":"2024-05-18T00:10:13Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Johannes Schindelin via GitGitGadget\" <gitgitgadget@gmail.com>\nwrites:\n\n> What the added protection did not anticipate is that such a\n> repository-local `core.hooksPath` can not only be used to point to\n> maliciously-placed scripts in the current worktree, but also to\n> _prevent_ hooks from being called altogether.\n> ...\n> diff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh\n> index f6dc83e2aab..1eae346a6e3 100755\n> --- a/t/t1350-config-hooks-path.sh\n> +++ b/t/t1350-config-hooks-path.sh\n> @@ -41,4 +41,8 @@ test_expect_success 'git rev-parse --git-path hooks' '\n>  \ttest .git/custom-hooks/abc = \"$(cat actual)\"\n>  '\n>  \n> +test_expect_success 'core.hooksPath=/dev/null' '\n> +\tgit clone -c core.hooksPath=/dev/null . no-templates\n> +'\n\nIs it sufficient that the command exits with 0?  I am wondering if\nwe want to verify that the resulting repository looks like it\nshould, e.g., with\n\n    v=$(git -C no-templates config --local --get core.hookspath) &&\n    test \"$v\" = /dev/null\n\nor something silly like that.\n\n\n"},{"id":"495017","messageId":"xmqqwmnsc6n8.fsf@gitster.g","threadId":"61509","inReplyTo":"98465797e72cf039ace4138ab1e03e4fc7465ea2.1715987756.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 6/8] hooks(clone protections): special-case current Git LFS hooks","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-18T00:20:43Z","receivedAt":"2024-05-18T00:20:48Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Johannes Schindelin via GitGitGadget\" <gitgitgadget@gmail.com>\nwrites:\n\n> +\t/* Hard-code known-safe values for Git LFS v3.4.0..v3.5.1 */\n> +\tif ((!strcmp(\"pre-push\", name) &&\n> +\t     !strcmp(sha256, \"df5417b2daa3aa144c19681d1e997df7ebfe144fb7e3e05138bd80ae998008e4\")) ||\n> +\t    (!strcmp(\"post-checkout\", name) &&\n> +\t     !strcmp(sha256, \"791471b4ff472aab844a4fceaa48bbb0a12193616f971e8e940625498b4938a6\")) ||\n> +\t    (!strcmp(\"post-commit\", name) &&\n> +\t     !strcmp(sha256, \"21e961572bb3f43a5f2fbafc1cc764d86046cc2e5f0bbecebfe9684a0b73b664\")) ||\n> +\t    (!strcmp(\"post-merge\", name) &&\n> +\t     !strcmp(sha256, \"75da0da66a803b4b030ad50801ba57062c6196105eb1d2251590d100edb9390b\")))\n> +\t\treturn 1;\n> +\n>  \tif (!safe_hook_sha256s_initialized) {\n>  \t\tsafe_hook_sha256s_initialized = 1;\n>  \t\tgit_protected_config(safe_hook_cb, &safe_hook_sha256s);\n\n\nNot that it makes a huge difference, but if I were doing this patch\nI would have instead added the special case hashes to the\nsafe_hook_sha256s hashmap inside this \"if we haven't been\ninitialized\" block.  That way, the checking of the hash can be done\nat the central place with the same code as used to check other\ncustom hooks.\n"},{"id":"495018","messageId":"xmqqseygc6lb.fsf@gitster.g","threadId":"61509","inReplyTo":"a4f5eeef6677462267d1dbf4b2b58d30aa013684.1715987756.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 5/8] hook(clone protections): add escape hatch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-18T00:21:52Z","receivedAt":"2024-05-18T00:21:58Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Johannes Schindelin via GitGitGadget\" <gitgitgadget@gmail.com>\nwrites:\n\n> +static int is_hook_safe_during_clone(const char *name, const char *path, char *sha256)\n> +{\n> +\tif (get_sha256_of_file_contents(path, sha256) < 0)\n> +\t\treturn 0;\n> +\n> +\tif (!safe_hook_sha256s_initialized) {\n> +\t\tsafe_hook_sha256s_initialized = 1;\n> +\t\tgit_protected_config(safe_hook_cb, &safe_hook_sha256s);\n> +\t}\n> +\n> +\treturn strset_contains(&safe_hook_sha256s, sha256);\n> +}\n\nMakes sense.  Use of a hashmap/strset here makes sense.\n"},{"id":"495025","messageId":"pull.1732.v2.git.1716028366.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.git.1715987756.gitgitgadget@gmail.com","subject":"[PATCH v2 0/8] Various fixes for v2.45.1 and friends","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-18T10:32:38Z","receivedAt":"2024-05-18T10:32:52Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"There have been a couple of issues that were reported about v2.45.1, and in\naddition I have noticed some myself:\n\n * a memory leak in the clone protection logic\n * a missed adjustment in the Makefile that leads to an incorrect templates\n   path in v2.39.4, v2.40.2 and v2.41.1 (but not in v2.42.2, ..., v2.45.1)\n * an overzealous core.hooksPath check\n * that Git LFS clone problem where it exits with an error (even if the\n   clone often succeeded...)\n\nThis patch series is based on maint-2.39 to allow for (relatively) easy\nfollow-up versions v2.39.5, ..., v2.45.2.\n\nChanges since v1:\n\n * simplified adding the SHA-256s corresponding to Git LFS' hooks\n * the core.hooksPath test case now verifies that the config setting was\n   configured correctly\n\nJohannes Schindelin (8):\n  hook: plug a new memory leak\n  init: use the correct path of the templates directory again\n  Revert \"core.hooksPath: add some protection while cloning\"\n  tests: verify that `clone -c core.hooksPath=/dev/null` works again\n  hook(clone protections): add escape hatch\n  hooks(clone protections): special-case current Git LFS hooks\n  hooks(clone protections): simplify templates hooks validation\n  Revert \"Add a helper function to compare file contents\"\n\n Documentation/config/safe.txt |   6 ++\n Makefile                      |   2 +-\n builtin/init-db.c             |   7 +++\n cache.h                       |  14 -----\n config.c                      |  13 +----\n copy.c                        |  58 --------------------\n hook.c                        | 100 +++++++++++++++++++++++++++-------\n hook.h                        |  10 ++++\n setup.c                       |   1 +\n t/helper/test-path-utils.c    |  10 ----\n t/t0060-path-utils.sh         |  41 --------------\n t/t1350-config-hooks-path.sh  |   7 +++\n t/t1800-hook.sh               |  40 ++++++++++----\n t/t5601-clone.sh              |  19 +++++++\n 14 files changed, 161 insertions(+), 167 deletions(-)\n\n\nbase-commit: 47b6d90e91835082010da926f6a844d4441c57a6\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-1732%2Fdscho%2Fvarious-fixes-for-v2.45.1-and-friends-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1732/dscho/various-fixes-for-v2.45.1-and-friends-v2\nPull-Request: https://github.com/gitgitgadget/git/pull/1732\n\nRange-diff vs v1:\n\n 1:  d4a003bf2ce = 1:  d4a003bf2ce hook: plug a new memory leak\n 2:  961dfc35f42 = 2:  961dfc35f42 init: use the correct path of the templates directory again\n 3:  57db89a1497 = 3:  57db89a1497 Revert \"core.hooksPath: add some protection while cloning\"\n 4:  7d5ef6db2a9 ! 4:  cd14042b065 tests: verify that `clone -c core.hooksPath=/dev/null` works again\n     @@ t/t1350-config-hooks-path.sh: test_expect_success 'git rev-parse --git-path hook\n       '\n       \n      +test_expect_success 'core.hooksPath=/dev/null' '\n     -+\tgit clone -c core.hooksPath=/dev/null . no-templates\n     ++\tgit clone -c core.hooksPath=/dev/null . no-templates &&\n     ++\tvalue=\"$(git -C no-templates config --local core.hooksPath)\" &&\n     ++\t# The Bash used by Git for Windows rewrites `/dev/null` to `nul`\n     ++\t{ test /dev/null = \"$value\" || test nul = \"$value\"; }\n      +'\n      +\n       test_done\n 5:  a4f5eeef667 = 5:  b841db8392e hook(clone protections): add escape hatch\n 6:  98465797e72 ! 6:  5e5128bc232 hooks(clone protections): special-case current Git LFS hooks\n     @@ Commit message\n      \n       ## hook.c ##\n      @@ hook.c: static int is_hook_safe_during_clone(const char *name, const char *path, char *s\n     - \tif (get_sha256_of_file_contents(path, sha256) < 0)\n     - \t\treturn 0;\n       \n     -+\t/* Hard-code known-safe values for Git LFS v3.4.0..v3.5.1 */\n     -+\tif ((!strcmp(\"pre-push\", name) &&\n     -+\t     !strcmp(sha256, \"df5417b2daa3aa144c19681d1e997df7ebfe144fb7e3e05138bd80ae998008e4\")) ||\n     -+\t    (!strcmp(\"post-checkout\", name) &&\n     -+\t     !strcmp(sha256, \"791471b4ff472aab844a4fceaa48bbb0a12193616f971e8e940625498b4938a6\")) ||\n     -+\t    (!strcmp(\"post-commit\", name) &&\n     -+\t     !strcmp(sha256, \"21e961572bb3f43a5f2fbafc1cc764d86046cc2e5f0bbecebfe9684a0b73b664\")) ||\n     -+\t    (!strcmp(\"post-merge\", name) &&\n     -+\t     !strcmp(sha256, \"75da0da66a803b4b030ad50801ba57062c6196105eb1d2251590d100edb9390b\")))\n     -+\t\treturn 1;\n     -+\n       \tif (!safe_hook_sha256s_initialized) {\n       \t\tsafe_hook_sha256s_initialized = 1;\n     ++\n     ++\t\t/* Hard-code known-safe values for Git LFS v3.4.0..v3.5.1 */\n     ++\t\t/* pre-push */\n     ++\t\tstrset_add(&safe_hook_sha256s, \"df5417b2daa3aa144c19681d1e997df7ebfe144fb7e3e05138bd80ae998008e4\");\n     ++\t\t/* post-checkout */\n     ++\t\tstrset_add(&safe_hook_sha256s, \"791471b4ff472aab844a4fceaa48bbb0a12193616f971e8e940625498b4938a6\");\n     ++\t\t/* post-commit */\n     ++\t\tstrset_add(&safe_hook_sha256s, \"21e961572bb3f43a5f2fbafc1cc764d86046cc2e5f0bbecebfe9684a0b73b664\");\n     ++\t\t/* post-merge */\n     ++\t\tstrset_add(&safe_hook_sha256s, \"75da0da66a803b4b030ad50801ba57062c6196105eb1d2251590d100edb9390b\");\n     ++\n       \t\tgit_protected_config(safe_hook_cb, &safe_hook_sha256s);\n     + \t}\n     + \n      \n       ## t/t1800-hook.sh ##\n      @@ t/t1800-hook.sh: test_expect_success '`safe.hook.sha256` and clone protections' '\n 7:  c487bd06be8 = 7:  bd6d72625f5 hooks(clone protections): simplify templates hooks validation\n 8:  c45c33d8e3f = 8:  4b0a636d41a Revert \"Add a helper function to compare file contents\"\n\n-- \ngitgitgadget\n"},{"id":"495026","messageId":"d4a003bf2ceafcc6d47d01d21b7faff48c9e85aa.1716028366.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.v2.git.1716028366.gitgitgadget@gmail.com","subject":"[PATCH v2 1/8] hook: plug a new memory leak","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-18T10:32:39Z","receivedAt":"2024-05-18T10:32:52Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nIn 8db1e8743c0 (clone: prevent hooks from running during a clone,\n2024-03-28), I introduced an inadvertent memory leak that was\nunfortunately not caught before v2.45.1 was released. Here is a fix.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n hook.c | 4 +++-\n 1 file changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/hook.c b/hook.c\nindex 632b537b993..fc974cee1d8 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -18,8 +18,10 @@ static int identical_to_template_hook(const char *name, const char *path)\n \t\tfound_template_hook = access(template_path.buf, X_OK) >= 0;\n \t}\n #endif\n-\tif (!found_template_hook)\n+\tif (!found_template_hook) {\n+\t\tstrbuf_release(&template_path);\n \t\treturn 0;\n+\t}\n \n \tret = do_files_match(template_path.buf, path);\n \n-- \ngitgitgadget\n\n"},{"id":"495027","messageId":"961dfc35f426388d660cca4e92f43e169819886a.1716028366.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.v2.git.1716028366.gitgitgadget@gmail.com","subject":"[PATCH v2 2/8] init: use the correct path of the templates directory again","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-18T10:32:40Z","receivedAt":"2024-05-18T10:32:53Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nIn df93e407f06 (init: refactor the template directory discovery into its\nown function, 2024-03-29), I refactored the way the templates directory\nis discovered.\n\nThe refactoring was faithful, but missed a reference in the `Makefile`\nwhere the `DEFAULT_GIT_TEMPLATE_DIR` constant is defined. As a\nconsequence, Git v2.45.1 and friends will always use the hard-coded path\n`/usr/share/git-core/templates`.\n\nLet's fix that by defining the `DEFAULT_GIT_TEMPLATE_DIR` when building\n`setup.o`, where that constant is actually used.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n Makefile | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/Makefile b/Makefile\nindex 093829ae283..4b1502ba2c6 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2751,7 +2751,7 @@ exec-cmd.sp exec-cmd.s exec-cmd.o: EXTRA_CPPFLAGS = \\\n \t'-DFALLBACK_RUNTIME_PREFIX=\"$(prefix_SQ)\"'\n \n builtin/init-db.sp builtin/init-db.s builtin/init-db.o: GIT-PREFIX\n-builtin/init-db.sp builtin/init-db.s builtin/init-db.o: EXTRA_CPPFLAGS = \\\n+setup.sp setup.s setup.o: EXTRA_CPPFLAGS = \\\n \t-DDEFAULT_GIT_TEMPLATE_DIR='\"$(template_dir_SQ)\"'\n \n config.sp config.s config.o: GIT-PREFIX\n-- \ngitgitgadget\n\n"},{"id":"495028","messageId":"57db89a14977bdff01f8f82cb4d6f85cc49d4b55.1716028366.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.v2.git.1716028366.gitgitgadget@gmail.com","subject":"[PATCH v2 3/8] Revert \"core.hooksPath: add some protection while cloning\"","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-18T10:32:41Z","receivedAt":"2024-05-18T10:32:55Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nThis defense-in-depth was intended to protect the clone operation\nagainst future escalations where bugs in `git clone` would allow\nattackers to write arbitrary files in the `.git/` directory would allow\nfor Remote Code Execution attacks via maliciously-placed hooks.\n\nHowever, it turns out that the `core.hooksPath` protection has\nunintentional side effects so severe that they do not justify the\nbenefit of the protections. For example, it has been reported in\nhttps://lore.kernel.org/git/FAFA34CB-9732-4A0A-87FB-BDB272E6AEE8@alchemists.io/\nthat the following invocation, which is intended to make `git clone`\nsafer, is itself broken by that protective measure:\n\n\tgit clone --config core.hooksPath=/dev/null <url>\n\nSince it turns out that the benefit does not justify the cost, let's revert\n20f3588efc6 (core.hooksPath: add some protection while cloning,\n2024-03-30).\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n config.c        | 13 +------------\n t/t1800-hook.sh | 15 ---------------\n 2 files changed, 1 insertion(+), 27 deletions(-)\n\ndiff --git a/config.c b/config.c\nindex 85b37f2ee09..8c1c4071f0d 100644\n--- a/config.c\n+++ b/config.c\n@@ -1525,19 +1525,8 @@ static int git_default_core_config(const char *var, const char *value, void *cb)\n \tif (!strcmp(var, \"core.attributesfile\"))\n \t\treturn git_config_pathname(&git_attributes_file, var, value);\n \n-\tif (!strcmp(var, \"core.hookspath\")) {\n-\t\tif (current_config_scope() == CONFIG_SCOPE_LOCAL &&\n-\t\t    git_env_bool(\"GIT_CLONE_PROTECTION_ACTIVE\", 0))\n-\t\t\tdie(_(\"active `core.hooksPath` found in the local \"\n-\t\t\t      \"repository config:\\n\\t%s\\nFor security \"\n-\t\t\t      \"reasons, this is disallowed by default.\\nIf \"\n-\t\t\t      \"this is intentional and the hook should \"\n-\t\t\t      \"actually be run, please\\nrun the command \"\n-\t\t\t      \"again with \"\n-\t\t\t      \"`GIT_CLONE_PROTECTION_ACTIVE=false`\"),\n-\t\t\t    value);\n+\tif (!strcmp(var, \"core.hookspath\"))\n \t\treturn git_config_pathname(&git_hooks_path, var, value);\n-\t}\n \n \tif (!strcmp(var, \"core.bare\")) {\n \t\tis_bare_repository_cfg = git_config_bool(var, value);\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 7ee12e6f48a..2ef3579fa7c 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -177,19 +177,4 @@ test_expect_success 'git hook run a hook with a bad shebang' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'clone protections' '\n-\ttest_config core.hooksPath \"$(pwd)/my-hooks\" &&\n-\tmkdir -p my-hooks &&\n-\twrite_script my-hooks/test-hook <<-\\EOF &&\n-\techo Hook ran $1\n-\tEOF\n-\n-\tgit hook run test-hook 2>err &&\n-\tgrep \"Hook ran\" err &&\n-\ttest_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \\\n-\t\tgit hook run test-hook 2>err &&\n-\tgrep \"active .core.hooksPath\" err &&\n-\t! grep \"Hook ran\" err\n-'\n-\n test_done\n-- \ngitgitgadget\n\n"},{"id":"495029","messageId":"cd14042b065e57f132c27d09005242fc500439e4.1716028366.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.v2.git.1716028366.gitgitgadget@gmail.com","subject":"[PATCH v2 4/8] tests: verify that `clone -c core.hooksPath=/dev/null` works again","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-18T10:32:42Z","receivedAt":"2024-05-18T10:32:56Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nAs part of the protections added in Git v2.45.1 and friends,\nrepository-local `core.hooksPath` settings are no longer allowed, as a\ndefense-in-depth mechanism to prevent future Git vulnerabilities to\nraise to critical level if those vulnerabilities inadvertently allow the\nrepository-local config to be written.\n\nWhat the added protection did not anticipate is that such a\nrepository-local `core.hooksPath` can not only be used to point to\nmaliciously-placed scripts in the current worktree, but also to\n_prevent_ hooks from being called altogether.\n\nWe just reverted the `core.hooksPath` protections, based on the Git\nmaintainer's recommendation in\nhttps://lore.kernel.org/git/xmqq4jaxvm8z.fsf@gitster.g/ to address this\nconcern as well as related ones. Let's make sure that we won't regress\nwhile trying to protect the clone operation further.\n\nReported-by: Brooke Kuhlmann <brooke@alchemists.io>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n t/t1350-config-hooks-path.sh | 7 +++++++\n 1 file changed, 7 insertions(+)\n\ndiff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh\nindex f6dc83e2aab..45a04929170 100755\n--- a/t/t1350-config-hooks-path.sh\n+++ b/t/t1350-config-hooks-path.sh\n@@ -41,4 +41,11 @@ test_expect_success 'git rev-parse --git-path hooks' '\n \ttest .git/custom-hooks/abc = \"$(cat actual)\"\n '\n \n+test_expect_success 'core.hooksPath=/dev/null' '\n+\tgit clone -c core.hooksPath=/dev/null . no-templates &&\n+\tvalue=\"$(git -C no-templates config --local core.hooksPath)\" &&\n+\t# The Bash used by Git for Windows rewrites `/dev/null` to `nul`\n+\t{ test /dev/null = \"$value\" || test nul = \"$value\"; }\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"495030","messageId":"b841db8392ebd924d1893829a7e5e22240f1e9cf.1716028366.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.v2.git.1716028366.gitgitgadget@gmail.com","subject":"[PATCH v2 5/8] hook(clone protections): add escape hatch","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-18T10:32:43Z","receivedAt":"2024-05-18T10:32:58Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nAs defense-in-depth measures, v2.39.4 and friends leading up to v2.45.1\nintroduced code that detects when hooks have been installed during a\n`git clone`, which is indicative of a common attack vector with critical\nseverity that allows Remote Code Execution.\n\nThere are legitimate use cases for such behavior, though, for example\nwhen those hooks stem from Git's own templates, which system\nadministrators are at liberty to modify to enforce, say, commit message\nconventions. The git clone protections specifically add exceptions to\nallow for that.\n\nAnother legitimate use case that has been identified too late to be\nhandled in these security bug-fix versions is Git LFS: It behaves\nsomewhat similar to common attack vectors by writing a few hooks while\nrunning the `smudge` filter during a regular clone, which means that Git\nhas no chance to know that the hooks are benign and e.g. the\n`post-checkout` hook can be safely executed as part of the clone\noperation.\n\nTo help Git LFS, and other tools behaving similarly (if there are any),\nlet's add a new, multi-valued `safe.hook.sha256` config setting. Like\nthe already-existing `safe.*` settings, it is ignored in\nrepository-local configs, and it is interpreted as a list of SHA-256\nchecksums of hooks' contents that are safe to execute during a clone\noperation. Future Git LFS versions will need to write those entries at\nthe same time they install the `smudge`/`clean` filters.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n Documentation/config/safe.txt |  6 ++++\n hook.c                        | 66 ++++++++++++++++++++++++++++++++---\n t/t1800-hook.sh               | 15 ++++++++\n 3 files changed, 82 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/config/safe.txt b/Documentation/config/safe.txt\nindex bde7f31459b..69ee845be89 100644\n--- a/Documentation/config/safe.txt\n+++ b/Documentation/config/safe.txt\n@@ -59,3 +59,9 @@ which id the original user has.\n If that is not what you would prefer and want git to only trust\n repositories that are owned by root instead, then you can remove\n the `SUDO_UID` variable from root's environment before invoking git.\n+\n+safe.hook.sha256::\n+\tThe value is the SHA-256 of hooks that are considered to be safe\n+\tto run during a clone operation.\n++\n+Multiple values can be added via `git config --global --add`.\ndiff --git a/hook.c b/hook.c\nindex fc974cee1d8..a2479738451 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -2,6 +2,7 @@\n #include \"hook.h\"\n #include \"run-command.h\"\n #include \"config.h\"\n+#include \"strmap.h\"\n \n static int identical_to_template_hook(const char *name, const char *path)\n {\n@@ -29,11 +30,65 @@ static int identical_to_template_hook(const char *name, const char *path)\n \treturn ret;\n }\n \n+static struct strset safe_hook_sha256s = STRSET_INIT;\n+static int safe_hook_sha256s_initialized;\n+\n+static int get_sha256_of_file_contents(const char *path, char *sha256)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tint fd;\n+\tssize_t res;\n+\n+\tgit_hash_ctx ctx;\n+\tconst struct git_hash_algo *algo = &hash_algos[GIT_HASH_SHA256];\n+\tunsigned char hash[GIT_MAX_RAWSZ];\n+\n+\tif ((fd = open(path, O_RDONLY)) < 0)\n+\t\treturn -1;\n+\tres = strbuf_read(&sb, fd, 400);\n+\tclose(fd);\n+\tif (res < 0)\n+\t\treturn -1;\n+\n+\talgo->init_fn(&ctx);\n+\talgo->update_fn(&ctx, sb.buf, sb.len);\n+\tstrbuf_release(&sb);\n+\talgo->final_fn(hash, &ctx);\n+\n+\thash_to_hex_algop_r(sha256, hash, algo);\n+\n+\treturn 0;\n+}\n+\n+static int safe_hook_cb(const char *key, const char *value, void *d)\n+{\n+\tstruct strset *set = d;\n+\n+\tif (value && !strcmp(key, \"safe.hook.sha256\"))\n+\t\tstrset_add(set, value);\n+\n+\treturn 0;\n+}\n+\n+static int is_hook_safe_during_clone(const char *name, const char *path, char *sha256)\n+{\n+\tif (get_sha256_of_file_contents(path, sha256) < 0)\n+\t\treturn 0;\n+\n+\tif (!safe_hook_sha256s_initialized) {\n+\t\tsafe_hook_sha256s_initialized = 1;\n+\t\tgit_protected_config(safe_hook_cb, &safe_hook_sha256s);\n+\t}\n+\n+\treturn strset_contains(&safe_hook_sha256s, sha256);\n+}\n+\n const char *find_hook(const char *name)\n {\n \tstatic struct strbuf path = STRBUF_INIT;\n \n \tint found_hook;\n+\tchar sha256[GIT_SHA256_HEXSZ + 1] = { '\\0' };\n \n \tstrbuf_reset(&path);\n \tstrbuf_git_path(&path, \"hooks/%s\", name);\n@@ -65,13 +120,14 @@ const char *find_hook(const char *name)\n \t\treturn NULL;\n \t}\n \tif (!git_hooks_path && git_env_bool(\"GIT_CLONE_PROTECTION_ACTIVE\", 0) &&\n-\t    !identical_to_template_hook(name, path.buf))\n+\t    !identical_to_template_hook(name, path.buf) &&\n+\t    !is_hook_safe_during_clone(name, path.buf, sha256))\n \t\tdie(_(\"active `%s` hook found during `git clone`:\\n\\t%s\\n\"\n \t\t      \"For security reasons, this is disallowed by default.\\n\"\n-\t\t      \"If this is intentional and the hook should actually \"\n-\t\t      \"be run, please\\nrun the command again with \"\n-\t\t      \"`GIT_CLONE_PROTECTION_ACTIVE=false`\"),\n-\t\t    name, path.buf);\n+\t\t      \"If this is intentional and the hook is safe to run, \"\n+\t\t      \"please run the following command and try again:\\n\\n\"\n+\t\t      \"  git config --global --add safe.hook.sha256 %s\"),\n+\t\t    name, path.buf, sha256);\n \treturn path.buf;\n }\n \ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 2ef3579fa7c..0f74c9154d0 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -177,4 +177,19 @@ test_expect_success 'git hook run a hook with a bad shebang' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success '`safe.hook.sha256` and clone protections' '\n+\tgit init safe-hook &&\n+\twrite_script safe-hook/.git/hooks/pre-push <<-\\EOF &&\n+\techo \"called hook\" >safe-hook.log\n+\tEOF\n+\n+\ttest_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \\\n+\t\tgit -C safe-hook hook run pre-push 2>err &&\n+\tcmd=\"$(grep \"git config --global --add safe.hook.sha256 [0-9a-f]\" err)\" &&\n+\teval \"$cmd\" &&\n+\tGIT_CLONE_PROTECTION_ACTIVE=true \\\n+\t\tgit -C safe-hook hook run pre-push &&\n+\ttest \"called hook\" = \"$(cat safe-hook/safe-hook.log)\"\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"495031","messageId":"5e5128bc232fbb822925efde20395484354492b6.1716028367.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.v2.git.1716028366.gitgitgadget@gmail.com","subject":"[PATCH v2 6/8] hooks(clone protections): special-case current Git LFS hooks","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-18T10:32:44Z","receivedAt":"2024-05-18T10:32:58Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nA notable regression in v2.45.1 and friends (all the way down to\nv2.39.4) has been that Git LFS-enabled clones error out with a message\nindicating that the `post-checkout` hook has been tampered with while\ncloning, and as a safety measure it is not executed.\n\nA generic fix for benign third-party applications wishing to write hooks\nduring clone operations has been implemented in the parent of this\ncommit: said applications are expected to add `safe.hook.sha256` values\nto a protected config.\n\nHowever, the current version of Git LFS, v3.5.1, cannot be adapted\nretroactively; Therefore, let's just hard-code the SHA-256 values for\nthis version. That way, Git LFS usage will no longer be broken, and the\nnext Git LFS version can be taught to add those `safe.hook.sha256`\nentries.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n hook.c          | 11 +++++++++++\n t/t1800-hook.sh | 20 ++++++++++++++++++++\n 2 files changed, 31 insertions(+)\n\ndiff --git a/hook.c b/hook.c\nindex a2479738451..f81b13df142 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -77,6 +77,17 @@ static int is_hook_safe_during_clone(const char *name, const char *path, char *s\n \n \tif (!safe_hook_sha256s_initialized) {\n \t\tsafe_hook_sha256s_initialized = 1;\n+\n+\t\t/* Hard-code known-safe values for Git LFS v3.4.0..v3.5.1 */\n+\t\t/* pre-push */\n+\t\tstrset_add(&safe_hook_sha256s, \"df5417b2daa3aa144c19681d1e997df7ebfe144fb7e3e05138bd80ae998008e4\");\n+\t\t/* post-checkout */\n+\t\tstrset_add(&safe_hook_sha256s, \"791471b4ff472aab844a4fceaa48bbb0a12193616f971e8e940625498b4938a6\");\n+\t\t/* post-commit */\n+\t\tstrset_add(&safe_hook_sha256s, \"21e961572bb3f43a5f2fbafc1cc764d86046cc2e5f0bbecebfe9684a0b73b664\");\n+\t\t/* post-merge */\n+\t\tstrset_add(&safe_hook_sha256s, \"75da0da66a803b4b030ad50801ba57062c6196105eb1d2251590d100edb9390b\");\n+\n \t\tgit_protected_config(safe_hook_cb, &safe_hook_sha256s);\n \t}\n \ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 0f74c9154d0..af66999aff3 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -192,4 +192,24 @@ test_expect_success '`safe.hook.sha256` and clone protections' '\n \ttest \"called hook\" = \"$(cat safe-hook/safe-hook.log)\"\n '\n \n+write_lfs_pre_push_hook () {\n+\twrite_script \"$1\" <<-\\EOF\n+\tcommand -v git-lfs >/dev/null 2>&1 || { echo >&2 \"\\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\\n\"; exit 2; }\n+\tgit lfs pre-push \"$@\"\n+\tEOF\n+}\n+\n+test_expect_success 'Git LFS special-handling in clone protections' '\n+\tgit init lfs-hooks &&\n+\twrite_lfs_pre_push_hook lfs-hooks/.git/hooks/pre-push &&\n+\twrite_script git-lfs <<-\\EOF &&\n+\techo \"called $*\" >fake-git-lfs.log\n+\tEOF\n+\n+\tPATH=\"$PWD:$PATH\" GIT_CLONE_PROTECTION_ACTIVE=true \\\n+\t\tgit -C lfs-hooks hook run pre-push &&\n+\ttest_write_lines \"called pre-push\" >expect &&\n+\ttest_cmp lfs-hooks/fake-git-lfs.log expect\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"495032","messageId":"bd6d72625f53e5983256da0ed7fe1a6431b3cfdf.1716028367.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.v2.git.1716028366.gitgitgadget@gmail.com","subject":"[PATCH v2 7/8] hooks(clone protections): simplify templates hooks validation","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-18T10:32:45Z","receivedAt":"2024-05-18T10:32:59Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nWhen an active hook is encountered during a clone operation, to protect\nagainst Remote Code Execution attack vectors, Git checks whether the\nhook was copied over from the templates directory.\n\nWhen that logic was introduced, there was no other way to check this\nthan to add a function to compare files.\n\nIn the meantime, we've added code to compute the SHA-256 checksum of a\ngiven hook and compare that checksum against a list of known-safe ones.\n\nLet's simplify the logic by adding to said list when copying the\ntemplates' hooks.\n\nWe need to be careful to support multi-process operations such as\nrecursive submodule clones: In such a scenario, the list of SHA-256\nchecksums that is kept in memory is not enough, we also have to pass the\ninformation down to child processes via `GIT_CONFIG_PARAMETERS`.\n\nExtend the regression test in t5601 to ensure that recursive clones are\nhandled as expected.\n\nNote: Technically there is no way that the checksums computed while\ninitializing the submodules' gitdirs can be passed to the process that\nperforms the checkout: For historical reasons, these operations are\nperformed in processes spawned in separate loops from the\nsuper-project's `git clone` process. But since the templates from which\nthe submodules are initialized are the very same as the ones from which\nthe super-project is initialized, we can get away with using the list of\nSHA-256 checksums that is computed when initializing the super-project\nand passing that down to the `submodule--helper` processes that perform\nthe recursive checkout.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n builtin/init-db.c |  7 +++++++\n hook.c            | 43 ++++++++++++++++---------------------------\n hook.h            | 10 ++++++++++\n setup.c           |  1 +\n t/t5601-clone.sh  | 19 +++++++++++++++++++\n 5 files changed, 53 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/init-db.c b/builtin/init-db.c\nindex a101e7f94c1..64357fdada4 100644\n--- a/builtin/init-db.c\n+++ b/builtin/init-db.c\n@@ -10,6 +10,8 @@\n #include \"exec-cmd.h\"\n #include \"parse-options.h\"\n #include \"worktree.h\"\n+#include \"run-command.h\"\n+#include \"hook.h\"\n \n #ifdef NO_TRUSTABLE_FILEMODE\n #define TEST_FILEMODE 0\n@@ -28,6 +30,7 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,\n \tsize_t path_baselen = path->len;\n \tsize_t template_baselen = template_path->len;\n \tstruct dirent *de;\n+\tint is_hooks_dir = ends_with(template_path->buf, \"/hooks/\");\n \n \t/* Note: if \".git/hooks\" file exists in the repository being\n \t * re-initialized, /etc/core-git/templates/hooks/update would\n@@ -80,6 +83,10 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,\n \t\t\tstrbuf_release(&lnk);\n \t\t}\n \t\telse if (S_ISREG(st_template.st_mode)) {\n+\t\t\tif (is_hooks_dir &&\n+\t\t\t    is_executable(template_path->buf))\n+\t\t\t\tadd_safe_hook(template_path->buf);\n+\n \t\t\tif (copy_file(path->buf, template_path->buf, st_template.st_mode))\n \t\t\t\tdie_errno(_(\"cannot copy '%s' to '%s'\"),\n \t\t\t\t\t  template_path->buf, path->buf);\ndiff --git a/hook.c b/hook.c\nindex f81b13df142..9e762cc9af6 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -4,32 +4,6 @@\n #include \"config.h\"\n #include \"strmap.h\"\n \n-static int identical_to_template_hook(const char *name, const char *path)\n-{\n-\tconst char *env = getenv(\"GIT_CLONE_TEMPLATE_DIR\");\n-\tconst char *template_dir = get_template_dir(env && *env ? env : NULL);\n-\tstruct strbuf template_path = STRBUF_INIT;\n-\tint found_template_hook, ret;\n-\n-\tstrbuf_addf(&template_path, \"%s/hooks/%s\", template_dir, name);\n-\tfound_template_hook = access(template_path.buf, X_OK) >= 0;\n-#ifdef STRIP_EXTENSION\n-\tif (!found_template_hook) {\n-\t\tstrbuf_addstr(&template_path, STRIP_EXTENSION);\n-\t\tfound_template_hook = access(template_path.buf, X_OK) >= 0;\n-\t}\n-#endif\n-\tif (!found_template_hook) {\n-\t\tstrbuf_release(&template_path);\n-\t\treturn 0;\n-\t}\n-\n-\tret = do_files_match(template_path.buf, path);\n-\n-\tstrbuf_release(&template_path);\n-\treturn ret;\n-}\n-\n static struct strset safe_hook_sha256s = STRSET_INIT;\n static int safe_hook_sha256s_initialized;\n \n@@ -60,6 +34,22 @@ static int get_sha256_of_file_contents(const char *path, char *sha256)\n \treturn 0;\n }\n \n+void add_safe_hook(const char *path)\n+{\n+\tchar sha256[GIT_SHA256_HEXSZ + 1] = { '\\0' };\n+\n+\tif (!get_sha256_of_file_contents(path, sha256)) {\n+\t\tchar *p;\n+\n+\t\tstrset_add(&safe_hook_sha256s, sha256);\n+\n+\t\t/* support multi-process operations e.g. recursive clones */\n+\t\tp = xstrfmt(\"safe.hook.sha256=%s\", sha256);\n+\t\tgit_config_push_parameter(p);\n+\t\tfree(p);\n+\t}\n+}\n+\n static int safe_hook_cb(const char *key, const char *value, void *d)\n {\n \tstruct strset *set = d;\n@@ -131,7 +121,6 @@ const char *find_hook(const char *name)\n \t\treturn NULL;\n \t}\n \tif (!git_hooks_path && git_env_bool(\"GIT_CLONE_PROTECTION_ACTIVE\", 0) &&\n-\t    !identical_to_template_hook(name, path.buf) &&\n \t    !is_hook_safe_during_clone(name, path.buf, sha256))\n \t\tdie(_(\"active `%s` hook found during `git clone`:\\n\\t%s\\n\"\n \t\t      \"For security reasons, this is disallowed by default.\\n\"\ndiff --git a/hook.h b/hook.h\nindex 4258b13da0d..e2034ee8b23 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -82,4 +82,14 @@ int run_hooks(const char *hook_name);\n  * hook. This function behaves like the old run_hook_le() API.\n  */\n int run_hooks_l(const char *hook_name, ...);\n+\n+/**\n+ * Mark the contents of the provided path as safe to run during a clone\n+ * operation.\n+ *\n+ * This function is mainly used when copying templates to mark the\n+ * just-copied hooks as benign.\n+ */\n+void add_safe_hook(const char *path);\n+\n #endif\ndiff --git a/setup.c b/setup.c\nindex c3301f5ab82..7f7538c9bf7 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -7,6 +7,7 @@\n #include \"promisor-remote.h\"\n #include \"quote.h\"\n #include \"exec-cmd.h\"\n+#include \"hook.h\"\n \n static int inside_git_dir = -1;\n static int inside_work_tree = -1;\ndiff --git a/t/t5601-clone.sh b/t/t5601-clone.sh\nindex 20deca0231b..71eaa3d1e14 100755\n--- a/t/t5601-clone.sh\n+++ b/t/t5601-clone.sh\n@@ -819,6 +819,25 @@ test_expect_success 'clone with init.templatedir runs hooks' '\n \t\tgit config --unset init.templateDir &&\n \t\t! grep \"active .* hook found\" err &&\n \t\ttest_path_is_missing hook-run-local-config/hook.run\n+\t) &&\n+\n+\ttest_config_global protocol.file.allow always &&\n+\tgit -C tmpl/hooks submodule add \"$(pwd)/tmpl/hooks\" sub &&\n+\ttest_tick &&\n+\tgit -C tmpl/hooks add .gitmodules sub &&\n+\tgit -C tmpl/hooks commit -m submodule &&\n+\n+\t(\n+\t\tsane_unset GIT_TEMPLATE_DIR &&\n+\t\tNO_SET_GIT_TEMPLATE_DIR=t &&\n+\t\texport NO_SET_GIT_TEMPLATE_DIR &&\n+\n+\t\tgit -c init.templateDir=\"$(pwd)/tmpl\" \\\n+\t\t\tclone --recurse-submodules \\\n+\t\t\ttmpl/hooks hook-run-submodule 2>err &&\n+\t\t! grep \"active .* hook found\" err &&\n+\t\ttest_path_is_file hook-run-submodule/hook.run &&\n+\t\ttest_path_is_file hook-run-submodule/sub/hook.run\n \t)\n '\n \n-- \ngitgitgadget\n\n"},{"id":"495033","messageId":"4b0a636d41a52513d57380589147a808840b4ad8.1716028367.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.v2.git.1716028366.gitgitgadget@gmail.com","subject":"[PATCH v2 8/8] Revert \"Add a helper function to compare file contents\"","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-18T10:32:46Z","receivedAt":"2024-05-18T10:33:01Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nNow that during a `git clone`, the hooks' contents are no longer\ncompared to the templates' files', the caller for which the\n`do_files_match()` function was introduced is gone, and therefore this\nfunction can be retired, too.\n\nThis reverts commit 584de0b4c23 (Add a helper function to compare file\ncontents, 2024-03-30).\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n cache.h                    | 14 ---------\n copy.c                     | 58 --------------------------------------\n t/helper/test-path-utils.c | 10 -------\n t/t0060-path-utils.sh      | 41 ---------------------------\n 4 files changed, 123 deletions(-)\n\ndiff --git a/cache.h b/cache.h\nindex 16b34799bfd..8c5fb1e1ba1 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1785,20 +1785,6 @@ int copy_fd(int ifd, int ofd);\n int copy_file(const char *dst, const char *src, int mode);\n int copy_file_with_time(const char *dst, const char *src, int mode);\n \n-/*\n- * Compare the file mode and contents of two given files.\n- *\n- * If both files are actually symbolic links, the function returns 1 if the link\n- * targets are identical or 0 if they are not.\n- *\n- * If any of the two files cannot be accessed or in case of read failures, this\n- * function returns 0.\n- *\n- * If the file modes and contents are identical, the function returns 1,\n- * otherwise it returns 0.\n- */\n-int do_files_match(const char *path1, const char *path2);\n-\n void write_or_die(int fd, const void *buf, size_t count);\n void fsync_or_die(int fd, const char *);\n int fsync_component(enum fsync_component component, int fd);\ndiff --git a/copy.c b/copy.c\nindex 8492f6fc831..4de6a110f09 100644\n--- a/copy.c\n+++ b/copy.c\n@@ -65,61 +65,3 @@ int copy_file_with_time(const char *dst, const char *src, int mode)\n \t\treturn copy_times(dst, src);\n \treturn status;\n }\n-\n-static int do_symlinks_match(const char *path1, const char *path2)\n-{\n-\tstruct strbuf buf1 = STRBUF_INIT, buf2 = STRBUF_INIT;\n-\tint ret = 0;\n-\n-\tif (!strbuf_readlink(&buf1, path1, 0) &&\n-\t    !strbuf_readlink(&buf2, path2, 0))\n-\t\tret = !strcmp(buf1.buf, buf2.buf);\n-\n-\tstrbuf_release(&buf1);\n-\tstrbuf_release(&buf2);\n-\treturn ret;\n-}\n-\n-int do_files_match(const char *path1, const char *path2)\n-{\n-\tstruct stat st1, st2;\n-\tint fd1 = -1, fd2 = -1, ret = 1;\n-\tchar buf1[8192], buf2[8192];\n-\n-\tif ((fd1 = open_nofollow(path1, O_RDONLY)) < 0 ||\n-\t    fstat(fd1, &st1) || !S_ISREG(st1.st_mode)) {\n-\t\tif (fd1 < 0 && errno == ELOOP)\n-\t\t\t/* maybe this is a symbolic link? */\n-\t\t\treturn do_symlinks_match(path1, path2);\n-\t\tret = 0;\n-\t} else if ((fd2 = open_nofollow(path2, O_RDONLY)) < 0 ||\n-\t\t   fstat(fd2, &st2) || !S_ISREG(st2.st_mode)) {\n-\t\tret = 0;\n-\t}\n-\n-\tif (ret)\n-\t\t/* to match, neither must be executable, or both */\n-\t\tret = !(st1.st_mode & 0111) == !(st2.st_mode & 0111);\n-\n-\tif (ret)\n-\t\tret = st1.st_size == st2.st_size;\n-\n-\twhile (ret) {\n-\t\tssize_t len1 = read_in_full(fd1, buf1, sizeof(buf1));\n-\t\tssize_t len2 = read_in_full(fd2, buf2, sizeof(buf2));\n-\n-\t\tif (len1 < 0 || len2 < 0 || len1 != len2)\n-\t\t\tret = 0; /* read error or different file size */\n-\t\telse if (!len1) /* len2 is also 0; hit EOF on both */\n-\t\t\tbreak; /* ret is still true */\n-\t\telse\n-\t\t\tret = !memcmp(buf1, buf2, len1);\n-\t}\n-\n-\tif (fd1 >= 0)\n-\t\tclose(fd1);\n-\tif (fd2 >= 0)\n-\t\tclose(fd2);\n-\n-\treturn ret;\n-}\ndiff --git a/t/helper/test-path-utils.c b/t/helper/test-path-utils.c\nindex 0e0de218076..f69709d674f 100644\n--- a/t/helper/test-path-utils.c\n+++ b/t/helper/test-path-utils.c\n@@ -495,16 +495,6 @@ int cmd__path_utils(int argc, const char **argv)\n \t\treturn !!res;\n \t}\n \n-\tif (argc == 4 && !strcmp(argv[1], \"do_files_match\")) {\n-\t\tint ret = do_files_match(argv[2], argv[3]);\n-\n-\t\tif (ret)\n-\t\t\tprintf(\"equal\\n\");\n-\t\telse\n-\t\t\tprintf(\"different\\n\");\n-\t\treturn !ret;\n-\t}\n-\n \tfprintf(stderr, \"%s: unknown function name: %s\\n\", argv[0],\n \t\targv[1] ? argv[1] : \"(there was none)\");\n \treturn 1;\ndiff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh\nindex 73d0e1a7f10..68e29c904a6 100755\n--- a/t/t0060-path-utils.sh\n+++ b/t/t0060-path-utils.sh\n@@ -560,45 +560,4 @@ test_expect_success !VALGRIND,RUNTIME_PREFIX,CAN_EXEC_IN_PWD '%(prefix)/ works'\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'do_files_match()' '\n-\ttest_seq 0 10 >0-10.txt &&\n-\ttest_seq -1 10 >-1-10.txt &&\n-\ttest_seq 1 10 >1-10.txt &&\n-\ttest_seq 1 9 >1-9.txt &&\n-\ttest_seq 0 8 >0-8.txt &&\n-\n-\ttest-tool path-utils do_files_match 0-10.txt 0-10.txt >out &&\n-\n-\tassert_fails() {\n-\t\ttest_must_fail \\\n-\t\ttest-tool path-utils do_files_match \"$1\" \"$2\" >out &&\n-\t\tgrep different out\n-\t} &&\n-\n-\tassert_fails 0-8.txt 1-9.txt &&\n-\tassert_fails -1-10.txt 0-10.txt &&\n-\tassert_fails 1-10.txt 1-9.txt &&\n-\tassert_fails 1-10.txt .git &&\n-\tassert_fails does-not-exist 1-10.txt &&\n-\n-\tif test_have_prereq FILEMODE\n-\tthen\n-\t\tcp 0-10.txt 0-10.x &&\n-\t\tchmod a+x 0-10.x &&\n-\t\tassert_fails 0-10.txt 0-10.x\n-\tfi &&\n-\n-\tif test_have_prereq SYMLINKS\n-\tthen\n-\t\tln -sf 0-10.txt symlink &&\n-\t\tln -s 0-10.txt another-symlink &&\n-\t\tln -s over-the-ocean yet-another-symlink &&\n-\t\tln -s \"$PWD/0-10.txt\" absolute-symlink &&\n-\t\tassert_fails 0-10.txt symlink &&\n-\t\ttest-tool path-utils do_files_match symlink another-symlink &&\n-\t\tassert_fails symlink yet-another-symlink &&\n-\t\tassert_fails symlink absolute-symlink\n-\tfi\n-'\n-\n test_done\n-- \ngitgitgadget\n"},{"id":"495035","messageId":"xmqqy187nj4u.fsf@gitster.g","threadId":"61509","inReplyTo":"pull.1732.v2.git.1716028366.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 0/8] Various fixes for v2.45.1 and friends","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-18T17:07:45Z","receivedAt":"2024-05-18T17:07:55Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"I have applied this to maint-2.39 and then merged them up to the\nmaintenance tracks.  The results will be pushed out to the \"split\nout\" repository at\n\n    https://github.com/gitster/git/\n\nas these branches:\n\n    js/fix-clone-w-hooks-2.39\n    js/fix-clone-w-hooks-2.40\n    js/fix-clone-w-hooks-2.41\n    js/fix-clone-w-hooks-2.42\n    js/fix-clone-w-hooks-2.43\n    js/fix-clone-w-hooks-2.44\n    js/fix-clone-w-hooks-2.45\n\nFor those who are merging them up at home yourselves, here are the\nremerge diff that show the conflict resolution I made.\n\n* js/fix-clone-w-hooks-2.40\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nremerge CONFLICT (content): Merge conflict in t/t1800-hook.sh\nindex 2af93d130d..bc0ed30ab9 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -177,7 +177,6 @@ test_expect_success 'git hook run a hook with a bad shebang' '\n \ttest_cmp expect actual\n '\n \n-<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< b9b439e0e3 (Git 2.40.2)\n test_expect_success 'stdin to hooks' '\n \twrite_script .git/hooks/test-hook <<-\\EOF &&\n \techo BEGIN stdin\n@@ -196,23 +195,10 @@ test_expect_success 'stdin to hooks' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'clone protections' '\n-\ttest_config core.hooksPath \"$(pwd)/my-hooks\" &&\n-\tmkdir -p my-hooks &&\n-\twrite_script my-hooks/test-hook <<-\\EOF &&\n-\techo Hook ran $1\n-|||||||||||||||||||||||||||||||| 47b6d90e91\n-test_expect_success 'clone protections' '\n-\ttest_config core.hooksPath \"$(pwd)/my-hooks\" &&\n-\tmkdir -p my-hooks &&\n-\twrite_script my-hooks/test-hook <<-\\EOF &&\n-\techo Hook ran $1\n-================================\n test_expect_success '`safe.hook.sha256` and clone protections' '\n \tgit init safe-hook &&\n \twrite_script safe-hook/.git/hooks/pre-push <<-\\EOF &&\n \techo \"called hook\" >safe-hook.log\n->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 851218a8af (Revert \"Add a helper function to compare file contents\")\n \tEOF\n \n \ttest_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \\\n* js/fix-clone-w-hooks-2.41\ndiff --git a/builtin/init-db.c b/builtin/init-db.c\nremerge CONFLICT (content): Merge conflict in builtin/init-db.c\nindex 0f0d2033c5..b351fe6e40 100644\n--- a/builtin/init-db.c\n+++ b/builtin/init-db.c\n@@ -17,13 +17,9 @@\n #include \"path.h\"\n #include \"setup.h\"\n #include \"worktree.h\"\n-<<<<<<< 0f15832059 (Git 2.41.1)\n #include \"wrapper.h\"\n-||||||| b9b439e0e3\n-=======\n #include \"run-command.h\"\n #include \"hook.h\"\n->>>>>>> 752e921355 (Merge branch 'js/fix-clone-w-hooks-2.39' into HEAD)\n \n #ifdef NO_TRUSTABLE_FILEMODE\n #define TEST_FILEMODE 0\ndiff --git a/cache.h b/cache.h\nremerge CONFLICT (content): Merge conflict in cache.h\nindex d99735c623..bdedb87e83 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -555,1301 +555,7 @@ extern int verify_ce_order;\n #define DATA_CHANGED    0x0020\n #define TYPE_CHANGED    0x0040\n \n-<<<<<<< 0f15832059 (Git 2.41.1)\n int cmp_cache_name_compare(const void *a_, const void *b_);\n-||||||| b9b439e0e3\n-/*\n- * Return an abbreviated sha1 unique within this repository's object database.\n- * The result will be at least `len` characters long, and will be NUL\n- * terminated.\n- *\n- * The non-`_r` version returns a static buffer which remains valid until 4\n- * more calls to find_unique_abbrev are made.\n- *\n- * The `_r` variant writes to a buffer supplied by the caller, which must be at\n- * least `GIT_MAX_HEXSZ + 1` bytes. The return value is the number of bytes\n- * written (excluding the NUL terminator).\n- *\n- * Note that while this version avoids the static buffer, it is not fully\n- * reentrant, as it calls into other non-reentrant git code.\n- */\n-const char *repo_find_unique_abbrev(struct repository *r, const struct object_id *oid, int len);\n-#define find_unique_abbrev(oid, len) repo_find_unique_abbrev(the_repository, oid, len)\n-int repo_find_unique_abbrev_r(struct repository *r, char *hex, const struct object_id *oid, int len);\n-#define find_unique_abbrev_r(hex, oid, len) repo_find_unique_abbrev_r(the_repository, hex, oid, len)\n-\n-/* set default permissions by passing mode arguments to open(2) */\n-int git_mkstemps_mode(char *pattern, int suffix_len, int mode);\n-int git_mkstemp_mode(char *pattern, int mode);\n-\n-/*\n- * NOTE NOTE NOTE!!\n- *\n- * PERM_UMASK, OLD_PERM_GROUP and OLD_PERM_EVERYBODY enumerations must\n- * not be changed. Old repositories have core.sharedrepository written in\n- * numeric format, and therefore these values are preserved for compatibility\n- * reasons.\n- */\n-enum sharedrepo {\n-\tPERM_UMASK          = 0,\n-\tOLD_PERM_GROUP      = 1,\n-\tOLD_PERM_EVERYBODY  = 2,\n-\tPERM_GROUP          = 0660,\n-\tPERM_EVERYBODY      = 0664\n-};\n-int git_config_perm(const char *var, const char *value);\n-int adjust_shared_perm(const char *path);\n-\n-/*\n- * Create the directory containing the named path, using care to be\n- * somewhat safe against races. Return one of the scld_error values to\n- * indicate success/failure. On error, set errno to describe the\n- * problem.\n- *\n- * SCLD_VANISHED indicates that one of the ancestor directories of the\n- * path existed at one point during the function call and then\n- * suddenly vanished, probably because another process pruned the\n- * directory while we were working.  To be robust against this kind of\n- * race, callers might want to try invoking the function again when it\n- * returns SCLD_VANISHED.\n- *\n- * safe_create_leading_directories() temporarily changes path while it\n- * is working but restores it before returning.\n- * safe_create_leading_directories_const() doesn't modify path, even\n- * temporarily. Both these variants adjust the permissions of the\n- * created directories to honor core.sharedRepository, so they are best\n- * suited for files inside the git dir. For working tree files, use\n- * safe_create_leading_directories_no_share() instead, as it ignores\n- * the core.sharedRepository setting.\n- */\n-enum scld_error {\n-\tSCLD_OK = 0,\n-\tSCLD_FAILED = -1,\n-\tSCLD_PERMS = -2,\n-\tSCLD_EXISTS = -3,\n-\tSCLD_VANISHED = -4\n-};\n-enum scld_error safe_create_leading_directories(char *path);\n-enum scld_error safe_create_leading_directories_const(const char *path);\n-enum scld_error safe_create_leading_directories_no_share(char *path);\n-\n-int mkdir_in_gitdir(const char *path);\n-char *interpolate_path(const char *path, int real_home);\n-/* NEEDSWORK: remove this synonym once in-flight topics have migrated */\n-#define expand_user_path interpolate_path\n-const char *enter_repo(const char *path, int strict);\n-static inline int is_absolute_path(const char *path)\n-{\n-\treturn is_dir_sep(path[0]) || has_dos_drive_prefix(path);\n-}\n-int is_directory(const char *);\n-char *strbuf_realpath(struct strbuf *resolved, const char *path,\n-\t\t      int die_on_error);\n-char *strbuf_realpath_forgiving(struct strbuf *resolved, const char *path,\n-\t\t\t\tint die_on_error);\n-char *real_pathdup(const char *path, int die_on_error);\n-const char *absolute_path(const char *path);\n-char *absolute_pathdup(const char *path);\n-const char *remove_leading_path(const char *in, const char *prefix);\n-const char *relative_path(const char *in, const char *prefix, struct strbuf *sb);\n-int normalize_path_copy_len(char *dst, const char *src, int *prefix_len);\n-int normalize_path_copy(char *dst, const char *src);\n-int longest_ancestor_length(const char *path, struct string_list *prefixes);\n-char *strip_path_suffix(const char *path, const char *suffix);\n-int daemon_avoid_alias(const char *path);\n-\n-/*\n- * These functions match their is_hfs_dotgit() counterparts; see utf8.h for\n- * details.\n- */\n-int is_ntfs_dotgit(const char *name);\n-int is_ntfs_dotgitmodules(const char *name);\n-int is_ntfs_dotgitignore(const char *name);\n-int is_ntfs_dotgitattributes(const char *name);\n-int is_ntfs_dotmailmap(const char *name);\n-\n-/*\n- * Returns true iff \"str\" could be confused as a command-line option when\n- * passed to a sub-program like \"ssh\". Note that this has nothing to do with\n- * shell-quoting, which should be handled separately; we're assuming here that\n- * the string makes it verbatim to the sub-program.\n- */\n-int looks_like_command_line_option(const char *str);\n-\n-/**\n- * Return a newly allocated string with the evaluation of\n- * \"$XDG_CONFIG_HOME/$subdir/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n- * \"$HOME/.config/$subdir/$filename\". Return NULL upon error.\n- */\n-char *xdg_config_home_for(const char *subdir, const char *filename);\n-\n-/**\n- * Return a newly allocated string with the evaluation of\n- * \"$XDG_CONFIG_HOME/git/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n- * \"$HOME/.config/git/$filename\". Return NULL upon error.\n- */\n-char *xdg_config_home(const char *filename);\n-\n-/**\n- * Return a newly allocated string with the evaluation of\n- * \"$XDG_CACHE_HOME/git/$filename\" if $XDG_CACHE_HOME is non-empty, otherwise\n- * \"$HOME/.cache/git/$filename\". Return NULL upon error.\n- */\n-char *xdg_cache_home(const char *filename);\n-\n-int git_open_cloexec(const char *name, int flags);\n-#define git_open(name) git_open_cloexec(name, O_RDONLY)\n-\n-/**\n- * unpack_loose_header() initializes the data stream needed to unpack\n- * a loose object header.\n- *\n- * Returns:\n- *\n- * - ULHR_OK on success\n- * - ULHR_BAD on error\n- * - ULHR_TOO_LONG if the header was too long\n- *\n- * It will only parse up to MAX_HEADER_LEN bytes unless an optional\n- * \"hdrbuf\" argument is non-NULL. This is intended for use with\n- * OBJECT_INFO_ALLOW_UNKNOWN_TYPE to extract the bad type for (error)\n- * reporting. The full header will be extracted to \"hdrbuf\" for use\n- * with parse_loose_header(), ULHR_TOO_LONG will still be returned\n- * from this function to indicate that the header was too long.\n- */\n-enum unpack_loose_header_result {\n-\tULHR_OK,\n-\tULHR_BAD,\n-\tULHR_TOO_LONG,\n-};\n-enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,\n-\t\t\t\t\t\t    unsigned char *map,\n-\t\t\t\t\t\t    unsigned long mapsize,\n-\t\t\t\t\t\t    void *buffer,\n-\t\t\t\t\t\t    unsigned long bufsiz,\n-\t\t\t\t\t\t    struct strbuf *hdrbuf);\n-\n-/**\n- * parse_loose_header() parses the starting \"<type> <len>\\0\" of an\n- * object. If it doesn't follow that format -1 is returned. To check\n- * the validity of the <type> populate the \"typep\" in the \"struct\n- * object_info\". It will be OBJ_BAD if the object type is unknown. The\n- * parsed <len> can be retrieved via \"oi->sizep\", and from there\n- * passed to unpack_loose_rest().\n- */\n-struct object_info;\n-int parse_loose_header(const char *hdr, struct object_info *oi);\n-\n-/**\n- * With in-core object data in \"buf\", rehash it to make sure the\n- * object name actually matches \"oid\" to detect object corruption.\n- *\n- * A negative value indicates an error, usually that the OID is not\n- * what we expected, but it might also indicate another error.\n- */\n-int check_object_signature(struct repository *r, const struct object_id *oid,\n-\t\t\t   void *map, unsigned long size,\n-\t\t\t   enum object_type type);\n-\n-/**\n- * A streaming version of check_object_signature().\n- * Try reading the object named with \"oid\" using\n- * the streaming interface and rehash it to do the same.\n- */\n-int stream_object_signature(struct repository *r, const struct object_id *oid);\n-\n-int finalize_object_file(const char *tmpfile, const char *filename);\n-\n-/* Helper to check and \"touch\" a file */\n-int check_and_freshen_file(const char *fn, int freshen);\n-\n-extern const signed char hexval_table[256];\n-static inline unsigned int hexval(unsigned char c)\n-{\n-\treturn hexval_table[c];\n-}\n-\n-/*\n- * Convert two consecutive hexadecimal digits into a char.  Return a\n- * negative value on error.  Don't run over the end of short strings.\n- */\n-static inline int hex2chr(const char *s)\n-{\n-\tunsigned int val = hexval(s[0]);\n-\treturn (val & ~0xf) ? val : (val << 4) | hexval(s[1]);\n-}\n-\n-/* Convert to/from hex/sha1 representation */\n-#define MINIMUM_ABBREV minimum_abbrev\n-#define DEFAULT_ABBREV default_abbrev\n-\n-/* used when the code does not know or care what the default abbrev is */\n-#define FALLBACK_DEFAULT_ABBREV 7\n-\n-struct object_context {\n-\tunsigned short mode;\n-\t/*\n-\t * symlink_path is only used by get_tree_entry_follow_symlinks,\n-\t * and only for symlinks that point outside the repository.\n-\t */\n-\tstruct strbuf symlink_path;\n-\t/*\n-\t * If GET_OID_RECORD_PATH is set, this will record path (if any)\n-\t * found when resolving the name. The caller is responsible for\n-\t * releasing the memory.\n-\t */\n-\tchar *path;\n-};\n-\n-#define GET_OID_QUIETLY           01\n-#define GET_OID_COMMIT            02\n-#define GET_OID_COMMITTISH        04\n-#define GET_OID_TREE             010\n-#define GET_OID_TREEISH          020\n-#define GET_OID_BLOB             040\n-#define GET_OID_FOLLOW_SYMLINKS 0100\n-#define GET_OID_RECORD_PATH     0200\n-#define GET_OID_ONLY_TO_DIE    04000\n-#define GET_OID_REQUIRE_PATH  010000\n-\n-#define GET_OID_DISAMBIGUATORS \\\n-\t(GET_OID_COMMIT | GET_OID_COMMITTISH | \\\n-\tGET_OID_TREE | GET_OID_TREEISH | \\\n-\tGET_OID_BLOB)\n-\n-enum get_oid_result {\n-\tFOUND = 0,\n-\tMISSING_OBJECT = -1, /* The requested object is missing */\n-\tSHORT_NAME_AMBIGUOUS = -2,\n-\t/* The following only apply when symlinks are followed */\n-\tDANGLING_SYMLINK = -4, /*\n-\t\t\t\t* The initial symlink is there, but\n-\t\t\t\t* (transitively) points to a missing\n-\t\t\t\t* in-tree file\n-\t\t\t\t*/\n-\tSYMLINK_LOOP = -5,\n-\tNOT_DIR = -6, /*\n-\t\t       * Somewhere along the symlink chain, a path is\n-\t\t       * requested which contains a file as a\n-\t\t       * non-final element.\n-\t\t       */\n-};\n-\n-int repo_get_oid(struct repository *r, const char *str, struct object_id *oid);\n-__attribute__((format (printf, 2, 3)))\n-int get_oidf(struct object_id *oid, const char *fmt, ...);\n-int repo_get_oid_commit(struct repository *r, const char *str, struct object_id *oid);\n-int repo_get_oid_committish(struct repository *r, const char *str, struct object_id *oid);\n-int repo_get_oid_tree(struct repository *r, const char *str, struct object_id *oid);\n-int repo_get_oid_treeish(struct repository *r, const char *str, struct object_id *oid);\n-int repo_get_oid_blob(struct repository *r, const char *str, struct object_id *oid);\n-int repo_get_oid_mb(struct repository *r, const char *str, struct object_id *oid);\n-void maybe_die_on_misspelt_object_name(struct repository *repo,\n-\t\t\t\t       const char *name,\n-\t\t\t\t       const char *prefix);\n-enum get_oid_result get_oid_with_context(struct repository *repo, const char *str,\n-\t\t\t\t\t unsigned flags, struct object_id *oid,\n-\t\t\t\t\t struct object_context *oc);\n-\n-#define get_oid(str, oid)\t\trepo_get_oid(the_repository, str, oid)\n-#define get_oid_commit(str, oid)\trepo_get_oid_commit(the_repository, str, oid)\n-#define get_oid_committish(str, oid)\trepo_get_oid_committish(the_repository, str, oid)\n-#define get_oid_tree(str, oid)\t\trepo_get_oid_tree(the_repository, str, oid)\n-#define get_oid_treeish(str, oid)\trepo_get_oid_treeish(the_repository, str, oid)\n-#define get_oid_blob(str, oid)\t\trepo_get_oid_blob(the_repository, str, oid)\n-#define get_oid_mb(str, oid) \t\trepo_get_oid_mb(the_repository, str, oid)\n-\n-typedef int each_abbrev_fn(const struct object_id *oid, void *);\n-int repo_for_each_abbrev(struct repository *r, const char *prefix, each_abbrev_fn, void *);\n-#define for_each_abbrev(prefix, fn, data) repo_for_each_abbrev(the_repository, prefix, fn, data)\n-\n-int set_disambiguate_hint_config(const char *var, const char *value);\n-\n-/*\n- * Try to read a SHA1 in hexadecimal format from the 40 characters\n- * starting at hex.  Write the 20-byte result to sha1 in binary form.\n- * Return 0 on success.  Reading stops if a NUL is encountered in the\n- * input, so it is safe to pass this function an arbitrary\n- * null-terminated string.\n- */\n-int get_sha1_hex(const char *hex, unsigned char *sha1);\n-int get_oid_hex(const char *hex, struct object_id *sha1);\n-\n-/* Like get_oid_hex, but for an arbitrary hash algorithm. */\n-int get_oid_hex_algop(const char *hex, struct object_id *oid, const struct git_hash_algo *algop);\n-\n-/*\n- * Read `len` pairs of hexadecimal digits from `hex` and write the\n- * values to `binary` as `len` bytes. Return 0 on success, or -1 if\n- * the input does not consist of hex digits).\n- */\n-int hex_to_bytes(unsigned char *binary, const char *hex, size_t len);\n-\n-/*\n- * Convert a binary hash in \"unsigned char []\" or an object name in\n- * \"struct object_id *\" to its hex equivalent. The `_r` variant is reentrant,\n- * and writes the NUL-terminated output to the buffer `out`, which must be at\n- * least `GIT_MAX_HEXSZ + 1` bytes, and returns a pointer to out for\n- * convenience.\n- *\n- * The non-`_r` variant returns a static buffer, but uses a ring of 4\n- * buffers, making it safe to make multiple calls for a single statement, like:\n- *\n- *   printf(\"%s -> %s\", hash_to_hex(one), hash_to_hex(two));\n- *   printf(\"%s -> %s\", oid_to_hex(one), oid_to_hex(two));\n- */\n-char *hash_to_hex_algop_r(char *buffer, const unsigned char *hash, const struct git_hash_algo *);\n-char *oid_to_hex_r(char *out, const struct object_id *oid);\n-char *hash_to_hex_algop(const unsigned char *hash, const struct git_hash_algo *);\t/* static buffer result! */\n-char *hash_to_hex(const unsigned char *hash);\t\t\t\t\t\t/* same static buffer */\n-char *oid_to_hex(const struct object_id *oid);\t\t\t\t\t\t/* same static buffer */\n-\n-/*\n- * Parse a 40-character hexadecimal object ID starting from hex, updating the\n- * pointer specified by end when parsing stops.  The resulting object ID is\n- * stored in oid.  Returns 0 on success.  Parsing will stop on the first NUL or\n- * other invalid character.  end is only updated on success; otherwise, it is\n- * unmodified.\n- */\n-int parse_oid_hex(const char *hex, struct object_id *oid, const char **end);\n-\n-/* Like parse_oid_hex, but for an arbitrary hash algorithm. */\n-int parse_oid_hex_algop(const char *hex, struct object_id *oid, const char **end,\n-\t\t\tconst struct git_hash_algo *algo);\n-\n-\n-/*\n- * These functions work like get_oid_hex and parse_oid_hex, but they will parse\n- * a hex value for any algorithm. The algorithm is detected based on the length\n- * and the algorithm in use is returned. If this is not a hex object ID in any\n- * algorithm, returns GIT_HASH_UNKNOWN.\n- */\n-int get_oid_hex_any(const char *hex, struct object_id *oid);\n-int parse_oid_hex_any(const char *hex, struct object_id *oid, const char **end);\n-\n-/*\n- * This reads short-hand syntax that not only evaluates to a commit\n- * object name, but also can act as if the end user spelled the name\n- * of the branch from the command line.\n- *\n- * - \"@{-N}\" finds the name of the Nth previous branch we were on, and\n- *   places the name of the branch in the given buf and returns the\n- *   number of characters parsed if successful.\n- *\n- * - \"<branch>@{upstream}\" finds the name of the other ref that\n- *   <branch> is configured to merge with (missing <branch> defaults\n- *   to the current branch), and places the name of the branch in the\n- *   given buf and returns the number of characters parsed if\n- *   successful.\n- *\n- * If the input is not of the accepted format, it returns a negative\n- * number to signal an error.\n- *\n- * If the input was ok but there are not N branch switches in the\n- * reflog, it returns 0.\n- */\n-#define INTERPRET_BRANCH_LOCAL (1<<0)\n-#define INTERPRET_BRANCH_REMOTE (1<<1)\n-#define INTERPRET_BRANCH_HEAD (1<<2)\n-struct interpret_branch_name_options {\n-\t/*\n-\t * If \"allowed\" is non-zero, it is a treated as a bitfield of allowable\n-\t * expansions: local branches (\"refs/heads/\"), remote branches\n-\t * (\"refs/remotes/\"), or \"HEAD\". If no \"allowed\" bits are set, any expansion is\n-\t * allowed, even ones to refs outside of those namespaces.\n-\t */\n-\tunsigned allowed;\n-\n-\t/*\n-\t * If ^{upstream} or ^{push} (or equivalent) is requested, and the\n-\t * branch in question does not have such a reference, return -1 instead\n-\t * of die()-ing.\n-\t */\n-\tunsigned nonfatal_dangling_mark : 1;\n-};\n-int repo_interpret_branch_name(struct repository *r,\n-\t\t\t       const char *str, int len,\n-\t\t\t       struct strbuf *buf,\n-\t\t\t       const struct interpret_branch_name_options *options);\n-#define interpret_branch_name(str, len, buf, options) \\\n-\trepo_interpret_branch_name(the_repository, str, len, buf, options)\n-\n-int validate_headref(const char *ref);\n-\n-int base_name_compare(const char *name1, size_t len1, int mode1,\n-\t\t      const char *name2, size_t len2, int mode2);\n-int df_name_compare(const char *name1, size_t len1, int mode1,\n-\t\t    const char *name2, size_t len2, int mode2);\n-int name_compare(const char *name1, size_t len1, const char *name2, size_t len2);\n-int cache_name_stage_compare(const char *name1, int len1, int stage1, const char *name2, int len2, int stage2);\n-\n-void *read_object_with_reference(struct repository *r,\n-\t\t\t\t const struct object_id *oid,\n-\t\t\t\t enum object_type required_type,\n-\t\t\t\t unsigned long *size,\n-\t\t\t\t struct object_id *oid_ret);\n-\n-struct object *repo_peel_to_type(struct repository *r,\n-\t\t\t\t const char *name, int namelen,\n-\t\t\t\t struct object *o, enum object_type);\n-#define peel_to_type(name, namelen, obj, type) \\\n-\trepo_peel_to_type(the_repository, name, namelen, obj, type)\n-\n-#define IDENT_STRICT\t       1\n-#define IDENT_NO_DATE\t       2\n-#define IDENT_NO_NAME\t       4\n-\n-enum want_ident {\n-\tWANT_BLANK_IDENT,\n-\tWANT_AUTHOR_IDENT,\n-\tWANT_COMMITTER_IDENT\n-};\n-\n-const char *git_author_info(int);\n-const char *git_committer_info(int);\n-const char *fmt_ident(const char *name, const char *email,\n-\t\t      enum want_ident whose_ident,\n-\t\t      const char *date_str, int);\n-const char *fmt_name(enum want_ident);\n-const char *ident_default_name(void);\n-const char *ident_default_email(void);\n-const char *git_editor(void);\n-const char *git_sequence_editor(void);\n-const char *git_pager(int stdout_is_tty);\n-int is_terminal_dumb(void);\n-int git_ident_config(const char *, const char *, void *);\n-/*\n- * Prepare an ident to fall back on if the user didn't configure it.\n- */\n-void prepare_fallback_ident(const char *name, const char *email);\n-void reset_ident_date(void);\n-\n-struct ident_split {\n-\tconst char *name_begin;\n-\tconst char *name_end;\n-\tconst char *mail_begin;\n-\tconst char *mail_end;\n-\tconst char *date_begin;\n-\tconst char *date_end;\n-\tconst char *tz_begin;\n-\tconst char *tz_end;\n-};\n-/*\n- * Signals an success with 0, but time part of the result may be NULL\n- * if the input lacks timestamp and zone\n- */\n-int split_ident_line(struct ident_split *, const char *, int);\n-\n-/*\n- * Given a commit or tag object buffer and the commit or tag headers, replaces\n- * the idents in the headers with their canonical versions using the mailmap mechanism.\n- */\n-void apply_mailmap_to_header(struct strbuf *, const char **, struct string_list *);\n-\n-/*\n- * Compare split idents for equality or strict ordering. Note that we\n- * compare only the ident part of the line, ignoring any timestamp.\n- *\n- * Because there are two fields, we must choose one as the primary key; we\n- * currently arbitrarily pick the email.\n- */\n-int ident_cmp(const struct ident_split *, const struct ident_split *);\n-\n-struct cache_def {\n-\tstruct strbuf path;\n-\tint flags;\n-\tint track_flags;\n-\tint prefix_len_stat_func;\n-};\n-#define CACHE_DEF_INIT { \\\n-\t.path = STRBUF_INIT, \\\n-}\n-static inline void cache_def_clear(struct cache_def *cache)\n-{\n-\tstrbuf_release(&cache->path);\n-}\n-\n-int has_symlink_leading_path(const char *name, int len);\n-int threaded_has_symlink_leading_path(struct cache_def *, const char *, int);\n-int check_leading_path(const char *name, int len, int warn_on_lstat_err);\n-int has_dirs_only_path(const char *name, int len, int prefix_len);\n-void invalidate_lstat_cache(void);\n-void schedule_dir_for_removal(const char *name, int len);\n-void remove_scheduled_dirs(void);\n-\n-struct pack_window {\n-\tstruct pack_window *next;\n-\tunsigned char *base;\n-\toff_t offset;\n-\tsize_t len;\n-\tunsigned int last_used;\n-\tunsigned int inuse_cnt;\n-};\n-\n-struct pack_entry {\n-\toff_t offset;\n-\tstruct packed_git *p;\n-};\n-\n-/*\n- * Create a temporary file rooted in the object database directory, or\n- * die on failure. The filename is taken from \"pattern\", which should have the\n- * usual \"XXXXXX\" trailer, and the resulting filename is written into the\n- * \"template\" buffer. Returns the open descriptor.\n- */\n-int odb_mkstemp(struct strbuf *temp_filename, const char *pattern);\n-\n-/*\n- * Create a pack .keep file named \"name\" (which should generally be the output\n- * of odb_pack_name). Returns a file descriptor opened for writing, or -1 on\n- * error.\n- */\n-int odb_pack_keep(const char *name);\n-\n-/*\n- * Set this to 0 to prevent oid_object_info_extended() from fetching missing\n- * blobs. This has a difference only if extensions.partialClone is set.\n- *\n- * Its default value is 1.\n- */\n-extern int fetch_if_missing;\n-\n-/* Dumb servers support */\n-int update_server_info(int);\n-\n-const char *get_log_output_encoding(void);\n-const char *get_commit_output_encoding(void);\n-\n-int committer_ident_sufficiently_given(void);\n-int author_ident_sufficiently_given(void);\n-\n-extern const char *git_commit_encoding;\n-extern const char *git_log_output_encoding;\n-extern const char *git_mailmap_file;\n-extern const char *git_mailmap_blob;\n-\n-/* IO helper functions */\n-void maybe_flush_or_die(FILE *, const char *);\n-__attribute__((format (printf, 2, 3)))\n-void fprintf_or_die(FILE *, const char *fmt, ...);\n-void fwrite_or_die(FILE *f, const void *buf, size_t count);\n-void fflush_or_die(FILE *f);\n-\n-#define COPY_READ_ERROR (-2)\n-#define COPY_WRITE_ERROR (-3)\n-int copy_fd(int ifd, int ofd);\n-int copy_file(const char *dst, const char *src, int mode);\n-int copy_file_with_time(const char *dst, const char *src, int mode);\n-\n-/*\n- * Compare the file mode and contents of two given files.\n- *\n- * If both files are actually symbolic links, the function returns 1 if the link\n- * targets are identical or 0 if they are not.\n- *\n- * If any of the two files cannot be accessed or in case of read failures, this\n- * function returns 0.\n- *\n- * If the file modes and contents are identical, the function returns 1,\n- * otherwise it returns 0.\n- */\n-int do_files_match(const char *path1, const char *path2);\n-\n-void write_or_die(int fd, const void *buf, size_t count);\n-void fsync_or_die(int fd, const char *);\n-int fsync_component(enum fsync_component component, int fd);\n-void fsync_component_or_die(enum fsync_component component, int fd, const char *msg);\n-\n-static inline int batch_fsync_enabled(enum fsync_component component)\n-{\n-\treturn (fsync_components & component) && (fsync_method == FSYNC_METHOD_BATCH);\n-}\n-\n-ssize_t read_in_full(int fd, void *buf, size_t count);\n-ssize_t write_in_full(int fd, const void *buf, size_t count);\n-ssize_t pread_in_full(int fd, void *buf, size_t count, off_t offset);\n-\n-static inline ssize_t write_str_in_full(int fd, const char *str)\n-{\n-\treturn write_in_full(fd, str, strlen(str));\n-}\n-\n-/**\n- * Open (and truncate) the file at path, write the contents of buf to it,\n- * and close it. Dies if any errors are encountered.\n- */\n-void write_file_buf(const char *path, const char *buf, size_t len);\n-\n-/**\n- * Like write_file_buf(), but format the contents into a buffer first.\n- * Additionally, write_file() will append a newline if one is not already\n- * present, making it convenient to write text files:\n- *\n- *   write_file(path, \"counter: %d\", ctr);\n- */\n-__attribute__((format (printf, 2, 3)))\n-void write_file(const char *path, const char *fmt, ...);\n-\n-/* pager.c */\n-void setup_pager(void);\n-int pager_in_use(void);\n-extern int pager_use_color;\n-int term_columns(void);\n-void term_clear_line(void);\n-int decimal_width(uintmax_t);\n-int check_pager_config(const char *cmd);\n-void prepare_pager_args(struct child_process *, const char *pager);\n-\n-extern const char *editor_program;\n-extern const char *askpass_program;\n-extern const char *excludes_file;\n-\n-/* base85 */\n-int decode_85(char *dst, const char *line, int linelen);\n-void encode_85(char *buf, const unsigned char *data, int bytes);\n-\n-/* pkt-line.c */\n-void packet_trace_identity(const char *prog);\n-=======\n-/*\n- * Return an abbreviated sha1 unique within this repository's object database.\n- * The result will be at least `len` characters long, and will be NUL\n- * terminated.\n- *\n- * The non-`_r` version returns a static buffer which remains valid until 4\n- * more calls to find_unique_abbrev are made.\n- *\n- * The `_r` variant writes to a buffer supplied by the caller, which must be at\n- * least `GIT_MAX_HEXSZ + 1` bytes. The return value is the number of bytes\n- * written (excluding the NUL terminator).\n- *\n- * Note that while this version avoids the static buffer, it is not fully\n- * reentrant, as it calls into other non-reentrant git code.\n- */\n-const char *repo_find_unique_abbrev(struct repository *r, const struct object_id *oid, int len);\n-#define find_unique_abbrev(oid, len) repo_find_unique_abbrev(the_repository, oid, len)\n-int repo_find_unique_abbrev_r(struct repository *r, char *hex, const struct object_id *oid, int len);\n-#define find_unique_abbrev_r(hex, oid, len) repo_find_unique_abbrev_r(the_repository, hex, oid, len)\n-\n-/* set default permissions by passing mode arguments to open(2) */\n-int git_mkstemps_mode(char *pattern, int suffix_len, int mode);\n-int git_mkstemp_mode(char *pattern, int mode);\n-\n-/*\n- * NOTE NOTE NOTE!!\n- *\n- * PERM_UMASK, OLD_PERM_GROUP and OLD_PERM_EVERYBODY enumerations must\n- * not be changed. Old repositories have core.sharedrepository written in\n- * numeric format, and therefore these values are preserved for compatibility\n- * reasons.\n- */\n-enum sharedrepo {\n-\tPERM_UMASK          = 0,\n-\tOLD_PERM_GROUP      = 1,\n-\tOLD_PERM_EVERYBODY  = 2,\n-\tPERM_GROUP          = 0660,\n-\tPERM_EVERYBODY      = 0664\n-};\n-int git_config_perm(const char *var, const char *value);\n-int adjust_shared_perm(const char *path);\n-\n-/*\n- * Create the directory containing the named path, using care to be\n- * somewhat safe against races. Return one of the scld_error values to\n- * indicate success/failure. On error, set errno to describe the\n- * problem.\n- *\n- * SCLD_VANISHED indicates that one of the ancestor directories of the\n- * path existed at one point during the function call and then\n- * suddenly vanished, probably because another process pruned the\n- * directory while we were working.  To be robust against this kind of\n- * race, callers might want to try invoking the function again when it\n- * returns SCLD_VANISHED.\n- *\n- * safe_create_leading_directories() temporarily changes path while it\n- * is working but restores it before returning.\n- * safe_create_leading_directories_const() doesn't modify path, even\n- * temporarily. Both these variants adjust the permissions of the\n- * created directories to honor core.sharedRepository, so they are best\n- * suited for files inside the git dir. For working tree files, use\n- * safe_create_leading_directories_no_share() instead, as it ignores\n- * the core.sharedRepository setting.\n- */\n-enum scld_error {\n-\tSCLD_OK = 0,\n-\tSCLD_FAILED = -1,\n-\tSCLD_PERMS = -2,\n-\tSCLD_EXISTS = -3,\n-\tSCLD_VANISHED = -4\n-};\n-enum scld_error safe_create_leading_directories(char *path);\n-enum scld_error safe_create_leading_directories_const(const char *path);\n-enum scld_error safe_create_leading_directories_no_share(char *path);\n-\n-int mkdir_in_gitdir(const char *path);\n-char *interpolate_path(const char *path, int real_home);\n-/* NEEDSWORK: remove this synonym once in-flight topics have migrated */\n-#define expand_user_path interpolate_path\n-const char *enter_repo(const char *path, int strict);\n-static inline int is_absolute_path(const char *path)\n-{\n-\treturn is_dir_sep(path[0]) || has_dos_drive_prefix(path);\n-}\n-int is_directory(const char *);\n-char *strbuf_realpath(struct strbuf *resolved, const char *path,\n-\t\t      int die_on_error);\n-char *strbuf_realpath_forgiving(struct strbuf *resolved, const char *path,\n-\t\t\t\tint die_on_error);\n-char *real_pathdup(const char *path, int die_on_error);\n-const char *absolute_path(const char *path);\n-char *absolute_pathdup(const char *path);\n-const char *remove_leading_path(const char *in, const char *prefix);\n-const char *relative_path(const char *in, const char *prefix, struct strbuf *sb);\n-int normalize_path_copy_len(char *dst, const char *src, int *prefix_len);\n-int normalize_path_copy(char *dst, const char *src);\n-int longest_ancestor_length(const char *path, struct string_list *prefixes);\n-char *strip_path_suffix(const char *path, const char *suffix);\n-int daemon_avoid_alias(const char *path);\n-\n-/*\n- * These functions match their is_hfs_dotgit() counterparts; see utf8.h for\n- * details.\n- */\n-int is_ntfs_dotgit(const char *name);\n-int is_ntfs_dotgitmodules(const char *name);\n-int is_ntfs_dotgitignore(const char *name);\n-int is_ntfs_dotgitattributes(const char *name);\n-int is_ntfs_dotmailmap(const char *name);\n-\n-/*\n- * Returns true iff \"str\" could be confused as a command-line option when\n- * passed to a sub-program like \"ssh\". Note that this has nothing to do with\n- * shell-quoting, which should be handled separately; we're assuming here that\n- * the string makes it verbatim to the sub-program.\n- */\n-int looks_like_command_line_option(const char *str);\n-\n-/**\n- * Return a newly allocated string with the evaluation of\n- * \"$XDG_CONFIG_HOME/$subdir/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n- * \"$HOME/.config/$subdir/$filename\". Return NULL upon error.\n- */\n-char *xdg_config_home_for(const char *subdir, const char *filename);\n-\n-/**\n- * Return a newly allocated string with the evaluation of\n- * \"$XDG_CONFIG_HOME/git/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n- * \"$HOME/.config/git/$filename\". Return NULL upon error.\n- */\n-char *xdg_config_home(const char *filename);\n-\n-/**\n- * Return a newly allocated string with the evaluation of\n- * \"$XDG_CACHE_HOME/git/$filename\" if $XDG_CACHE_HOME is non-empty, otherwise\n- * \"$HOME/.cache/git/$filename\". Return NULL upon error.\n- */\n-char *xdg_cache_home(const char *filename);\n-\n-int git_open_cloexec(const char *name, int flags);\n-#define git_open(name) git_open_cloexec(name, O_RDONLY)\n-\n-/**\n- * unpack_loose_header() initializes the data stream needed to unpack\n- * a loose object header.\n- *\n- * Returns:\n- *\n- * - ULHR_OK on success\n- * - ULHR_BAD on error\n- * - ULHR_TOO_LONG if the header was too long\n- *\n- * It will only parse up to MAX_HEADER_LEN bytes unless an optional\n- * \"hdrbuf\" argument is non-NULL. This is intended for use with\n- * OBJECT_INFO_ALLOW_UNKNOWN_TYPE to extract the bad type for (error)\n- * reporting. The full header will be extracted to \"hdrbuf\" for use\n- * with parse_loose_header(), ULHR_TOO_LONG will still be returned\n- * from this function to indicate that the header was too long.\n- */\n-enum unpack_loose_header_result {\n-\tULHR_OK,\n-\tULHR_BAD,\n-\tULHR_TOO_LONG,\n-};\n-enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,\n-\t\t\t\t\t\t    unsigned char *map,\n-\t\t\t\t\t\t    unsigned long mapsize,\n-\t\t\t\t\t\t    void *buffer,\n-\t\t\t\t\t\t    unsigned long bufsiz,\n-\t\t\t\t\t\t    struct strbuf *hdrbuf);\n-\n-/**\n- * parse_loose_header() parses the starting \"<type> <len>\\0\" of an\n- * object. If it doesn't follow that format -1 is returned. To check\n- * the validity of the <type> populate the \"typep\" in the \"struct\n- * object_info\". It will be OBJ_BAD if the object type is unknown. The\n- * parsed <len> can be retrieved via \"oi->sizep\", and from there\n- * passed to unpack_loose_rest().\n- */\n-struct object_info;\n-int parse_loose_header(const char *hdr, struct object_info *oi);\n-\n-/**\n- * With in-core object data in \"buf\", rehash it to make sure the\n- * object name actually matches \"oid\" to detect object corruption.\n- *\n- * A negative value indicates an error, usually that the OID is not\n- * what we expected, but it might also indicate another error.\n- */\n-int check_object_signature(struct repository *r, const struct object_id *oid,\n-\t\t\t   void *map, unsigned long size,\n-\t\t\t   enum object_type type);\n-\n-/**\n- * A streaming version of check_object_signature().\n- * Try reading the object named with \"oid\" using\n- * the streaming interface and rehash it to do the same.\n- */\n-int stream_object_signature(struct repository *r, const struct object_id *oid);\n-\n-int finalize_object_file(const char *tmpfile, const char *filename);\n-\n-/* Helper to check and \"touch\" a file */\n-int check_and_freshen_file(const char *fn, int freshen);\n-\n-extern const signed char hexval_table[256];\n-static inline unsigned int hexval(unsigned char c)\n-{\n-\treturn hexval_table[c];\n-}\n-\n-/*\n- * Convert two consecutive hexadecimal digits into a char.  Return a\n- * negative value on error.  Don't run over the end of short strings.\n- */\n-static inline int hex2chr(const char *s)\n-{\n-\tunsigned int val = hexval(s[0]);\n-\treturn (val & ~0xf) ? val : (val << 4) | hexval(s[1]);\n-}\n-\n-/* Convert to/from hex/sha1 representation */\n-#define MINIMUM_ABBREV minimum_abbrev\n-#define DEFAULT_ABBREV default_abbrev\n-\n-/* used when the code does not know or care what the default abbrev is */\n-#define FALLBACK_DEFAULT_ABBREV 7\n-\n-struct object_context {\n-\tunsigned short mode;\n-\t/*\n-\t * symlink_path is only used by get_tree_entry_follow_symlinks,\n-\t * and only for symlinks that point outside the repository.\n-\t */\n-\tstruct strbuf symlink_path;\n-\t/*\n-\t * If GET_OID_RECORD_PATH is set, this will record path (if any)\n-\t * found when resolving the name. The caller is responsible for\n-\t * releasing the memory.\n-\t */\n-\tchar *path;\n-};\n-\n-#define GET_OID_QUIETLY           01\n-#define GET_OID_COMMIT            02\n-#define GET_OID_COMMITTISH        04\n-#define GET_OID_TREE             010\n-#define GET_OID_TREEISH          020\n-#define GET_OID_BLOB             040\n-#define GET_OID_FOLLOW_SYMLINKS 0100\n-#define GET_OID_RECORD_PATH     0200\n-#define GET_OID_ONLY_TO_DIE    04000\n-#define GET_OID_REQUIRE_PATH  010000\n-\n-#define GET_OID_DISAMBIGUATORS \\\n-\t(GET_OID_COMMIT | GET_OID_COMMITTISH | \\\n-\tGET_OID_TREE | GET_OID_TREEISH | \\\n-\tGET_OID_BLOB)\n-\n-enum get_oid_result {\n-\tFOUND = 0,\n-\tMISSING_OBJECT = -1, /* The requested object is missing */\n-\tSHORT_NAME_AMBIGUOUS = -2,\n-\t/* The following only apply when symlinks are followed */\n-\tDANGLING_SYMLINK = -4, /*\n-\t\t\t\t* The initial symlink is there, but\n-\t\t\t\t* (transitively) points to a missing\n-\t\t\t\t* in-tree file\n-\t\t\t\t*/\n-\tSYMLINK_LOOP = -5,\n-\tNOT_DIR = -6, /*\n-\t\t       * Somewhere along the symlink chain, a path is\n-\t\t       * requested which contains a file as a\n-\t\t       * non-final element.\n-\t\t       */\n-};\n-\n-int repo_get_oid(struct repository *r, const char *str, struct object_id *oid);\n-__attribute__((format (printf, 2, 3)))\n-int get_oidf(struct object_id *oid, const char *fmt, ...);\n-int repo_get_oid_commit(struct repository *r, const char *str, struct object_id *oid);\n-int repo_get_oid_committish(struct repository *r, const char *str, struct object_id *oid);\n-int repo_get_oid_tree(struct repository *r, const char *str, struct object_id *oid);\n-int repo_get_oid_treeish(struct repository *r, const char *str, struct object_id *oid);\n-int repo_get_oid_blob(struct repository *r, const char *str, struct object_id *oid);\n-int repo_get_oid_mb(struct repository *r, const char *str, struct object_id *oid);\n-void maybe_die_on_misspelt_object_name(struct repository *repo,\n-\t\t\t\t       const char *name,\n-\t\t\t\t       const char *prefix);\n-enum get_oid_result get_oid_with_context(struct repository *repo, const char *str,\n-\t\t\t\t\t unsigned flags, struct object_id *oid,\n-\t\t\t\t\t struct object_context *oc);\n-\n-#define get_oid(str, oid)\t\trepo_get_oid(the_repository, str, oid)\n-#define get_oid_commit(str, oid)\trepo_get_oid_commit(the_repository, str, oid)\n-#define get_oid_committish(str, oid)\trepo_get_oid_committish(the_repository, str, oid)\n-#define get_oid_tree(str, oid)\t\trepo_get_oid_tree(the_repository, str, oid)\n-#define get_oid_treeish(str, oid)\trepo_get_oid_treeish(the_repository, str, oid)\n-#define get_oid_blob(str, oid)\t\trepo_get_oid_blob(the_repository, str, oid)\n-#define get_oid_mb(str, oid) \t\trepo_get_oid_mb(the_repository, str, oid)\n-\n-typedef int each_abbrev_fn(const struct object_id *oid, void *);\n-int repo_for_each_abbrev(struct repository *r, const char *prefix, each_abbrev_fn, void *);\n-#define for_each_abbrev(prefix, fn, data) repo_for_each_abbrev(the_repository, prefix, fn, data)\n-\n-int set_disambiguate_hint_config(const char *var, const char *value);\n-\n-/*\n- * Try to read a SHA1 in hexadecimal format from the 40 characters\n- * starting at hex.  Write the 20-byte result to sha1 in binary form.\n- * Return 0 on success.  Reading stops if a NUL is encountered in the\n- * input, so it is safe to pass this function an arbitrary\n- * null-terminated string.\n- */\n-int get_sha1_hex(const char *hex, unsigned char *sha1);\n-int get_oid_hex(const char *hex, struct object_id *sha1);\n-\n-/* Like get_oid_hex, but for an arbitrary hash algorithm. */\n-int get_oid_hex_algop(const char *hex, struct object_id *oid, const struct git_hash_algo *algop);\n-\n-/*\n- * Read `len` pairs of hexadecimal digits from `hex` and write the\n- * values to `binary` as `len` bytes. Return 0 on success, or -1 if\n- * the input does not consist of hex digits).\n- */\n-int hex_to_bytes(unsigned char *binary, const char *hex, size_t len);\n-\n-/*\n- * Convert a binary hash in \"unsigned char []\" or an object name in\n- * \"struct object_id *\" to its hex equivalent. The `_r` variant is reentrant,\n- * and writes the NUL-terminated output to the buffer `out`, which must be at\n- * least `GIT_MAX_HEXSZ + 1` bytes, and returns a pointer to out for\n- * convenience.\n- *\n- * The non-`_r` variant returns a static buffer, but uses a ring of 4\n- * buffers, making it safe to make multiple calls for a single statement, like:\n- *\n- *   printf(\"%s -> %s\", hash_to_hex(one), hash_to_hex(two));\n- *   printf(\"%s -> %s\", oid_to_hex(one), oid_to_hex(two));\n- */\n-char *hash_to_hex_algop_r(char *buffer, const unsigned char *hash, const struct git_hash_algo *);\n-char *oid_to_hex_r(char *out, const struct object_id *oid);\n-char *hash_to_hex_algop(const unsigned char *hash, const struct git_hash_algo *);\t/* static buffer result! */\n-char *hash_to_hex(const unsigned char *hash);\t\t\t\t\t\t/* same static buffer */\n-char *oid_to_hex(const struct object_id *oid);\t\t\t\t\t\t/* same static buffer */\n-\n-/*\n- * Parse a 40-character hexadecimal object ID starting from hex, updating the\n- * pointer specified by end when parsing stops.  The resulting object ID is\n- * stored in oid.  Returns 0 on success.  Parsing will stop on the first NUL or\n- * other invalid character.  end is only updated on success; otherwise, it is\n- * unmodified.\n- */\n-int parse_oid_hex(const char *hex, struct object_id *oid, const char **end);\n-\n-/* Like parse_oid_hex, but for an arbitrary hash algorithm. */\n-int parse_oid_hex_algop(const char *hex, struct object_id *oid, const char **end,\n-\t\t\tconst struct git_hash_algo *algo);\n-\n-\n-/*\n- * These functions work like get_oid_hex and parse_oid_hex, but they will parse\n- * a hex value for any algorithm. The algorithm is detected based on the length\n- * and the algorithm in use is returned. If this is not a hex object ID in any\n- * algorithm, returns GIT_HASH_UNKNOWN.\n- */\n-int get_oid_hex_any(const char *hex, struct object_id *oid);\n-int parse_oid_hex_any(const char *hex, struct object_id *oid, const char **end);\n-\n-/*\n- * This reads short-hand syntax that not only evaluates to a commit\n- * object name, but also can act as if the end user spelled the name\n- * of the branch from the command line.\n- *\n- * - \"@{-N}\" finds the name of the Nth previous branch we were on, and\n- *   places the name of the branch in the given buf and returns the\n- *   number of characters parsed if successful.\n- *\n- * - \"<branch>@{upstream}\" finds the name of the other ref that\n- *   <branch> is configured to merge with (missing <branch> defaults\n- *   to the current branch), and places the name of the branch in the\n- *   given buf and returns the number of characters parsed if\n- *   successful.\n- *\n- * If the input is not of the accepted format, it returns a negative\n- * number to signal an error.\n- *\n- * If the input was ok but there are not N branch switches in the\n- * reflog, it returns 0.\n- */\n-#define INTERPRET_BRANCH_LOCAL (1<<0)\n-#define INTERPRET_BRANCH_REMOTE (1<<1)\n-#define INTERPRET_BRANCH_HEAD (1<<2)\n-struct interpret_branch_name_options {\n-\t/*\n-\t * If \"allowed\" is non-zero, it is a treated as a bitfield of allowable\n-\t * expansions: local branches (\"refs/heads/\"), remote branches\n-\t * (\"refs/remotes/\"), or \"HEAD\". If no \"allowed\" bits are set, any expansion is\n-\t * allowed, even ones to refs outside of those namespaces.\n-\t */\n-\tunsigned allowed;\n-\n-\t/*\n-\t * If ^{upstream} or ^{push} (or equivalent) is requested, and the\n-\t * branch in question does not have such a reference, return -1 instead\n-\t * of die()-ing.\n-\t */\n-\tunsigned nonfatal_dangling_mark : 1;\n-};\n-int repo_interpret_branch_name(struct repository *r,\n-\t\t\t       const char *str, int len,\n-\t\t\t       struct strbuf *buf,\n-\t\t\t       const struct interpret_branch_name_options *options);\n-#define interpret_branch_name(str, len, buf, options) \\\n-\trepo_interpret_branch_name(the_repository, str, len, buf, options)\n-\n-int validate_headref(const char *ref);\n-\n-int base_name_compare(const char *name1, size_t len1, int mode1,\n-\t\t      const char *name2, size_t len2, int mode2);\n-int df_name_compare(const char *name1, size_t len1, int mode1,\n-\t\t    const char *name2, size_t len2, int mode2);\n-int name_compare(const char *name1, size_t len1, const char *name2, size_t len2);\n-int cache_name_stage_compare(const char *name1, int len1, int stage1, const char *name2, int len2, int stage2);\n-\n-void *read_object_with_reference(struct repository *r,\n-\t\t\t\t const struct object_id *oid,\n-\t\t\t\t enum object_type required_type,\n-\t\t\t\t unsigned long *size,\n-\t\t\t\t struct object_id *oid_ret);\n-\n-struct object *repo_peel_to_type(struct repository *r,\n-\t\t\t\t const char *name, int namelen,\n-\t\t\t\t struct object *o, enum object_type);\n-#define peel_to_type(name, namelen, obj, type) \\\n-\trepo_peel_to_type(the_repository, name, namelen, obj, type)\n-\n-#define IDENT_STRICT\t       1\n-#define IDENT_NO_DATE\t       2\n-#define IDENT_NO_NAME\t       4\n-\n-enum want_ident {\n-\tWANT_BLANK_IDENT,\n-\tWANT_AUTHOR_IDENT,\n-\tWANT_COMMITTER_IDENT\n-};\n-\n-const char *git_author_info(int);\n-const char *git_committer_info(int);\n-const char *fmt_ident(const char *name, const char *email,\n-\t\t      enum want_ident whose_ident,\n-\t\t      const char *date_str, int);\n-const char *fmt_name(enum want_ident);\n-const char *ident_default_name(void);\n-const char *ident_default_email(void);\n-const char *git_editor(void);\n-const char *git_sequence_editor(void);\n-const char *git_pager(int stdout_is_tty);\n-int is_terminal_dumb(void);\n-int git_ident_config(const char *, const char *, void *);\n-/*\n- * Prepare an ident to fall back on if the user didn't configure it.\n- */\n-void prepare_fallback_ident(const char *name, const char *email);\n-void reset_ident_date(void);\n-\n-struct ident_split {\n-\tconst char *name_begin;\n-\tconst char *name_end;\n-\tconst char *mail_begin;\n-\tconst char *mail_end;\n-\tconst char *date_begin;\n-\tconst char *date_end;\n-\tconst char *tz_begin;\n-\tconst char *tz_end;\n-};\n-/*\n- * Signals an success with 0, but time part of the result may be NULL\n- * if the input lacks timestamp and zone\n- */\n-int split_ident_line(struct ident_split *, const char *, int);\n-\n-/*\n- * Given a commit or tag object buffer and the commit or tag headers, replaces\n- * the idents in the headers with their canonical versions using the mailmap mechanism.\n- */\n-void apply_mailmap_to_header(struct strbuf *, const char **, struct string_list *);\n-\n-/*\n- * Compare split idents for equality or strict ordering. Note that we\n- * compare only the ident part of the line, ignoring any timestamp.\n- *\n- * Because there are two fields, we must choose one as the primary key; we\n- * currently arbitrarily pick the email.\n- */\n-int ident_cmp(const struct ident_split *, const struct ident_split *);\n-\n-struct cache_def {\n-\tstruct strbuf path;\n-\tint flags;\n-\tint track_flags;\n-\tint prefix_len_stat_func;\n-};\n-#define CACHE_DEF_INIT { \\\n-\t.path = STRBUF_INIT, \\\n-}\n-static inline void cache_def_clear(struct cache_def *cache)\n-{\n-\tstrbuf_release(&cache->path);\n-}\n-\n-int has_symlink_leading_path(const char *name, int len);\n-int threaded_has_symlink_leading_path(struct cache_def *, const char *, int);\n-int check_leading_path(const char *name, int len, int warn_on_lstat_err);\n-int has_dirs_only_path(const char *name, int len, int prefix_len);\n-void invalidate_lstat_cache(void);\n-void schedule_dir_for_removal(const char *name, int len);\n-void remove_scheduled_dirs(void);\n-\n-struct pack_window {\n-\tstruct pack_window *next;\n-\tunsigned char *base;\n-\toff_t offset;\n-\tsize_t len;\n-\tunsigned int last_used;\n-\tunsigned int inuse_cnt;\n-};\n-\n-struct pack_entry {\n-\toff_t offset;\n-\tstruct packed_git *p;\n-};\n-\n-/*\n- * Create a temporary file rooted in the object database directory, or\n- * die on failure. The filename is taken from \"pattern\", which should have the\n- * usual \"XXXXXX\" trailer, and the resulting filename is written into the\n- * \"template\" buffer. Returns the open descriptor.\n- */\n-int odb_mkstemp(struct strbuf *temp_filename, const char *pattern);\n-\n-/*\n- * Create a pack .keep file named \"name\" (which should generally be the output\n- * of odb_pack_name). Returns a file descriptor opened for writing, or -1 on\n- * error.\n- */\n-int odb_pack_keep(const char *name);\n-\n-/*\n- * Set this to 0 to prevent oid_object_info_extended() from fetching missing\n- * blobs. This has a difference only if extensions.partialClone is set.\n- *\n- * Its default value is 1.\n- */\n-extern int fetch_if_missing;\n-\n-/* Dumb servers support */\n-int update_server_info(int);\n-\n-const char *get_log_output_encoding(void);\n-const char *get_commit_output_encoding(void);\n-\n-int committer_ident_sufficiently_given(void);\n-int author_ident_sufficiently_given(void);\n-\n-extern const char *git_commit_encoding;\n-extern const char *git_log_output_encoding;\n-extern const char *git_mailmap_file;\n-extern const char *git_mailmap_blob;\n-\n-/* IO helper functions */\n-void maybe_flush_or_die(FILE *, const char *);\n-__attribute__((format (printf, 2, 3)))\n-void fprintf_or_die(FILE *, const char *fmt, ...);\n-void fwrite_or_die(FILE *f, const void *buf, size_t count);\n-void fflush_or_die(FILE *f);\n-\n-#define COPY_READ_ERROR (-2)\n-#define COPY_WRITE_ERROR (-3)\n-int copy_fd(int ifd, int ofd);\n-int copy_file(const char *dst, const char *src, int mode);\n-int copy_file_with_time(const char *dst, const char *src, int mode);\n-\n-void write_or_die(int fd, const void *buf, size_t count);\n-void fsync_or_die(int fd, const char *);\n-int fsync_component(enum fsync_component component, int fd);\n-void fsync_component_or_die(enum fsync_component component, int fd, const char *msg);\n-\n-static inline int batch_fsync_enabled(enum fsync_component component)\n-{\n-\treturn (fsync_components & component) && (fsync_method == FSYNC_METHOD_BATCH);\n-}\n-\n-ssize_t read_in_full(int fd, void *buf, size_t count);\n-ssize_t write_in_full(int fd, const void *buf, size_t count);\n-ssize_t pread_in_full(int fd, void *buf, size_t count, off_t offset);\n-\n-static inline ssize_t write_str_in_full(int fd, const char *str)\n-{\n-\treturn write_in_full(fd, str, strlen(str));\n-}\n-\n-/**\n- * Open (and truncate) the file at path, write the contents of buf to it,\n- * and close it. Dies if any errors are encountered.\n- */\n-void write_file_buf(const char *path, const char *buf, size_t len);\n-\n-/**\n- * Like write_file_buf(), but format the contents into a buffer first.\n- * Additionally, write_file() will append a newline if one is not already\n- * present, making it convenient to write text files:\n- *\n- *   write_file(path, \"counter: %d\", ctr);\n- */\n-__attribute__((format (printf, 2, 3)))\n-void write_file(const char *path, const char *fmt, ...);\n-\n-/* pager.c */\n-void setup_pager(void);\n-int pager_in_use(void);\n-extern int pager_use_color;\n-int term_columns(void);\n-void term_clear_line(void);\n-int decimal_width(uintmax_t);\n-int check_pager_config(const char *cmd);\n-void prepare_pager_args(struct child_process *, const char *pager);\n-\n-extern const char *editor_program;\n-extern const char *askpass_program;\n-extern const char *excludes_file;\n-\n-/* base85 */\n-int decode_85(char *dst, const char *line, int linelen);\n-void encode_85(char *buf, const unsigned char *data, int bytes);\n-\n-/* pkt-line.c */\n-void packet_trace_identity(const char *prog);\n->>>>>>> 752e921355 (Merge branch 'js/fix-clone-w-hooks-2.39' into HEAD)\n \n /* add */\n /*\ndiff --git a/hook.c b/hook.c\nremerge CONFLICT (content): Merge conflict in hook.c\nindex ce71f9d01a..b56f97949b 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -5,15 +5,12 @@\n #include \"path.h\"\n #include \"run-command.h\"\n #include \"config.h\"\n-<<<<<<< 0f15832059 (Git 2.41.1)\n #include \"strbuf.h\"\n #include \"environment.h\"\n #include \"setup.h\"\n #include \"copy.h\"\n-||||||| b9b439e0e3\n-=======\n #include \"strmap.h\"\n->>>>>>> 752e921355 (Merge branch 'js/fix-clone-w-hooks-2.39' into HEAD)\n+#include \"hex.h\"\n \n static struct strset safe_hook_sha256s = STRSET_INIT;\n static int safe_hook_sha256s_initialized;\n* js/fix-clone-w-hooks-2.42\ndiff --git a/Makefile b/Makefile\nremerge CONFLICT (content): Merge conflict in Makefile\nindex 5ae5f3c864..5776309365 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2743,16 +2743,8 @@ exec-cmd.sp exec-cmd.s exec-cmd.o: EXTRA_CPPFLAGS = \\\n \t'-DBINDIR=\"$(bindir_relative_SQ)\"' \\\n \t'-DFALLBACK_RUNTIME_PREFIX=\"$(prefix_SQ)\"'\n \n-<<<<<<< babb4e5d71 (Git 2.42.2)\n setup.sp setup.s setup.o: GIT-PREFIX\n setup.sp setup.s setup.o: EXTRA_CPPFLAGS = \\\n-||||||| 0f15832059\n-builtin/init-db.sp builtin/init-db.s builtin/init-db.o: GIT-PREFIX\n-builtin/init-db.sp builtin/init-db.s builtin/init-db.o: EXTRA_CPPFLAGS = \\\n-=======\n-builtin/init-db.sp builtin/init-db.s builtin/init-db.o: GIT-PREFIX\n-setup.sp setup.s setup.o: EXTRA_CPPFLAGS = \\\n->>>>>>> c7b6b0a28f (Merge branch 'js/fix-clone-w-hooks-2.40' into HEAD)\n \t-DDEFAULT_GIT_TEMPLATE_DIR='\"$(template_dir_SQ)\"'\n \n config.sp config.s config.o: GIT-PREFIX\ndiff --git a/builtin/init-db.c b/builtin/init-db.c\nremerge CONFLICT (content): Merge conflict in builtin/init-db.c\nindex 6df47dd078..cb727c826f 100644\n--- a/builtin/init-db.c\n+++ b/builtin/init-db.c\n@@ -12,920 +12,7 @@\n #include \"parse-options.h\"\n #include \"path.h\"\n #include \"setup.h\"\n-<<<<<<< babb4e5d71 (Git 2.42.2)\n #include \"strbuf.h\"\n-||||||| 0f15832059\n-#include \"worktree.h\"\n-#include \"wrapper.h\"\n-\n-#ifdef NO_TRUSTABLE_FILEMODE\n-#define TEST_FILEMODE 0\n-#else\n-#define TEST_FILEMODE 1\n-#endif\n-\n-#define GIT_DEFAULT_HASH_ENVIRONMENT \"GIT_DEFAULT_HASH\"\n-\n-static int init_is_bare_repository = 0;\n-static int init_shared_repository = -1;\n-\n-static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,\n-\t\t\t     DIR *dir)\n-{\n-\tsize_t path_baselen = path->len;\n-\tsize_t template_baselen = template_path->len;\n-\tstruct dirent *de;\n-\n-\t/* Note: if \".git/hooks\" file exists in the repository being\n-\t * re-initialized, /etc/core-git/templates/hooks/update would\n-\t * cause \"git init\" to fail here.  I think this is sane but\n-\t * it means that the set of templates we ship by default, along\n-\t * with the way the namespace under .git/ is organized, should\n-\t * be really carefully chosen.\n-\t */\n-\tsafe_create_dir(path->buf, 1);\n-\twhile ((de = readdir(dir)) != NULL) {\n-\t\tstruct stat st_git, st_template;\n-\t\tint exists = 0;\n-\n-\t\tstrbuf_setlen(path, path_baselen);\n-\t\tstrbuf_setlen(template_path, template_baselen);\n-\n-\t\tif (de->d_name[0] == '.')\n-\t\t\tcontinue;\n-\t\tstrbuf_addstr(path, de->d_name);\n-\t\tstrbuf_addstr(template_path, de->d_name);\n-\t\tif (lstat(path->buf, &st_git)) {\n-\t\t\tif (errno != ENOENT)\n-\t\t\t\tdie_errno(_(\"cannot stat '%s'\"), path->buf);\n-\t\t}\n-\t\telse\n-\t\t\texists = 1;\n-\n-\t\tif (lstat(template_path->buf, &st_template))\n-\t\t\tdie_errno(_(\"cannot stat template '%s'\"), template_path->buf);\n-\n-\t\tif (S_ISDIR(st_template.st_mode)) {\n-\t\t\tDIR *subdir = opendir(template_path->buf);\n-\t\t\tif (!subdir)\n-\t\t\t\tdie_errno(_(\"cannot opendir '%s'\"), template_path->buf);\n-\t\t\tstrbuf_addch(path, '/');\n-\t\t\tstrbuf_addch(template_path, '/');\n-\t\t\tcopy_templates_1(path, template_path, subdir);\n-\t\t\tclosedir(subdir);\n-\t\t}\n-\t\telse if (exists)\n-\t\t\tcontinue;\n-\t\telse if (S_ISLNK(st_template.st_mode)) {\n-\t\t\tstruct strbuf lnk = STRBUF_INIT;\n-\t\t\tif (strbuf_readlink(&lnk, template_path->buf,\n-\t\t\t\t\t    st_template.st_size) < 0)\n-\t\t\t\tdie_errno(_(\"cannot readlink '%s'\"), template_path->buf);\n-\t\t\tif (symlink(lnk.buf, path->buf))\n-\t\t\t\tdie_errno(_(\"cannot symlink '%s' '%s'\"),\n-\t\t\t\t\t  lnk.buf, path->buf);\n-\t\t\tstrbuf_release(&lnk);\n-\t\t}\n-\t\telse if (S_ISREG(st_template.st_mode)) {\n-\t\t\tif (copy_file(path->buf, template_path->buf, st_template.st_mode))\n-\t\t\t\tdie_errno(_(\"cannot copy '%s' to '%s'\"),\n-\t\t\t\t\t  template_path->buf, path->buf);\n-\t\t}\n-\t\telse\n-\t\t\terror(_(\"ignoring template %s\"), template_path->buf);\n-\t}\n-}\n-\n-static void copy_templates(const char *option_template)\n-{\n-\tconst char *template_dir = get_template_dir(option_template);\n-\tstruct strbuf path = STRBUF_INIT;\n-\tstruct strbuf template_path = STRBUF_INIT;\n-\tsize_t template_len;\n-\tstruct repository_format template_format = REPOSITORY_FORMAT_INIT;\n-\tstruct strbuf err = STRBUF_INIT;\n-\tDIR *dir;\n-\tchar *to_free = NULL;\n-\n-\tif (!template_dir || !*template_dir)\n-\t\treturn;\n-\n-\tstrbuf_addstr(&template_path, template_dir);\n-\tstrbuf_complete(&template_path, '/');\n-\ttemplate_len = template_path.len;\n-\n-\tdir = opendir(template_path.buf);\n-\tif (!dir) {\n-\t\twarning(_(\"templates not found in %s\"), template_dir);\n-\t\tgoto free_return;\n-\t}\n-\n-\t/* Make sure that template is from the correct vintage */\n-\tstrbuf_addstr(&template_path, \"config\");\n-\tread_repository_format(&template_format, template_path.buf);\n-\tstrbuf_setlen(&template_path, template_len);\n-\n-\t/*\n-\t * No mention of version at all is OK, but anything else should be\n-\t * verified.\n-\t */\n-\tif (template_format.version >= 0 &&\n-\t    verify_repository_format(&template_format, &err) < 0) {\n-\t\twarning(_(\"not copying templates from '%s': %s\"),\n-\t\t\t  template_dir, err.buf);\n-\t\tstrbuf_release(&err);\n-\t\tgoto close_free_return;\n-\t}\n-\n-\tstrbuf_addstr(&path, get_git_common_dir());\n-\tstrbuf_complete(&path, '/');\n-\tcopy_templates_1(&path, &template_path, dir);\n-close_free_return:\n-\tclosedir(dir);\n-free_return:\n-\tfree(to_free);\n-\tstrbuf_release(&path);\n-\tstrbuf_release(&template_path);\n-\tclear_repository_format(&template_format);\n-}\n-\n-/*\n- * If the git_dir is not directly inside the working tree, then git will not\n- * find it by default, and we need to set the worktree explicitly.\n- */\n-static int needs_work_tree_config(const char *git_dir, const char *work_tree)\n-{\n-\tif (!strcmp(work_tree, \"/\") && !strcmp(git_dir, \"/.git\"))\n-\t\treturn 0;\n-\tif (skip_prefix(git_dir, work_tree, &git_dir) &&\n-\t    !strcmp(git_dir, \"/.git\"))\n-\t\treturn 0;\n-\treturn 1;\n-}\n-\n-void initialize_repository_version(int hash_algo, int reinit)\n-{\n-\tchar repo_version_string[10];\n-\tint repo_version = GIT_REPO_VERSION;\n-\n-\tif (hash_algo != GIT_HASH_SHA1)\n-\t\trepo_version = GIT_REPO_VERSION_READ;\n-\n-\t/* This forces creation of new config file */\n-\txsnprintf(repo_version_string, sizeof(repo_version_string),\n-\t\t  \"%d\", repo_version);\n-\tgit_config_set(\"core.repositoryformatversion\", repo_version_string);\n-\n-\tif (hash_algo != GIT_HASH_SHA1)\n-\t\tgit_config_set(\"extensions.objectformat\",\n-\t\t\t       hash_algos[hash_algo].name);\n-\telse if (reinit)\n-\t\tgit_config_set_gently(\"extensions.objectformat\", NULL);\n-}\n-\n-static int create_default_files(const char *template_path,\n-\t\t\t\tconst char *original_git_dir,\n-\t\t\t\tconst char *initial_branch,\n-\t\t\t\tconst struct repository_format *fmt,\n-\t\t\t\tint quiet)\n-{\n-\tstruct stat st1;\n-\tstruct strbuf buf = STRBUF_INIT;\n-\tchar *path;\n-\tchar junk[2];\n-\tint reinit;\n-\tint filemode;\n-\tstruct strbuf err = STRBUF_INIT;\n-\tconst char *work_tree = get_git_work_tree();\n-\n-\t/*\n-\t * First copy the templates -- we might have the default\n-\t * config file there, in which case we would want to read\n-\t * from it after installing.\n-\t *\n-\t * Before reading that config, we also need to clear out any cached\n-\t * values (since we've just potentially changed what's available on\n-\t * disk).\n-\t */\n-\tcopy_templates(template_path);\n-\tgit_config_clear();\n-\treset_shared_repository();\n-\tgit_config(git_default_config, NULL);\n-\n-\t/*\n-\t * We must make sure command-line options continue to override any\n-\t * values we might have just re-read from the config.\n-\t */\n-\tis_bare_repository_cfg = init_is_bare_repository || !work_tree;\n-\tif (init_shared_repository != -1)\n-\t\tset_shared_repository(init_shared_repository);\n-\n-\t/*\n-\t * We would have created the above under user's umask -- under\n-\t * shared-repository settings, we would need to fix them up.\n-\t */\n-\tif (get_shared_repository()) {\n-\t\tadjust_shared_perm(get_git_dir());\n-\t}\n-\n-\t/*\n-\t * We need to create a \"refs\" dir in any case so that older\n-\t * versions of git can tell that this is a repository.\n-\t */\n-\tsafe_create_dir(git_path(\"refs\"), 1);\n-\tadjust_shared_perm(git_path(\"refs\"));\n-\n-\tif (refs_init_db(&err))\n-\t\tdie(\"failed to set up refs db: %s\", err.buf);\n-\n-\t/*\n-\t * Point the HEAD symref to the initial branch with if HEAD does\n-\t * not yet exist.\n-\t */\n-\tpath = git_path_buf(&buf, \"HEAD\");\n-\treinit = (!access(path, R_OK)\n-\t\t  || readlink(path, junk, sizeof(junk)-1) != -1);\n-\tif (!reinit) {\n-\t\tchar *ref;\n-\n-\t\tif (!initial_branch)\n-\t\t\tinitial_branch = git_default_branch_name(quiet);\n-\n-\t\tref = xstrfmt(\"refs/heads/%s\", initial_branch);\n-\t\tif (check_refname_format(ref, 0) < 0)\n-\t\t\tdie(_(\"invalid initial branch name: '%s'\"),\n-\t\t\t    initial_branch);\n-\n-\t\tif (create_symref(\"HEAD\", ref, NULL) < 0)\n-\t\t\texit(1);\n-\t\tfree(ref);\n-\t}\n-\n-\tinitialize_repository_version(fmt->hash_algo, 0);\n-\n-\t/* Check filemode trustability */\n-\tpath = git_path_buf(&buf, \"config\");\n-\tfilemode = TEST_FILEMODE;\n-\tif (TEST_FILEMODE && !lstat(path, &st1)) {\n-\t\tstruct stat st2;\n-\t\tfilemode = (!chmod(path, st1.st_mode ^ S_IXUSR) &&\n-\t\t\t\t!lstat(path, &st2) &&\n-\t\t\t\tst1.st_mode != st2.st_mode &&\n-\t\t\t\t!chmod(path, st1.st_mode));\n-\t\tif (filemode && !reinit && (st1.st_mode & S_IXUSR))\n-\t\t\tfilemode = 0;\n-\t}\n-\tgit_config_set(\"core.filemode\", filemode ? \"true\" : \"false\");\n-\n-\tif (is_bare_repository())\n-\t\tgit_config_set(\"core.bare\", \"true\");\n-\telse {\n-\t\tgit_config_set(\"core.bare\", \"false\");\n-\t\t/* allow template config file to override the default */\n-\t\tif (log_all_ref_updates == LOG_REFS_UNSET)\n-\t\t\tgit_config_set(\"core.logallrefupdates\", \"true\");\n-\t\tif (needs_work_tree_config(original_git_dir, work_tree))\n-\t\t\tgit_config_set(\"core.worktree\", work_tree);\n-\t}\n-\n-\tif (!reinit) {\n-\t\t/* Check if symlink is supported in the work tree */\n-\t\tpath = git_path_buf(&buf, \"tXXXXXX\");\n-\t\tif (!close(xmkstemp(path)) &&\n-\t\t    !unlink(path) &&\n-\t\t    !symlink(\"testing\", path) &&\n-\t\t    !lstat(path, &st1) &&\n-\t\t    S_ISLNK(st1.st_mode))\n-\t\t\tunlink(path); /* good */\n-\t\telse\n-\t\t\tgit_config_set(\"core.symlinks\", \"false\");\n-\n-\t\t/* Check if the filesystem is case-insensitive */\n-\t\tpath = git_path_buf(&buf, \"CoNfIg\");\n-\t\tif (!access(path, F_OK))\n-\t\t\tgit_config_set(\"core.ignorecase\", \"true\");\n-\t\tprobe_utf8_pathname_composition();\n-\t}\n-\n-\tstrbuf_release(&buf);\n-\treturn reinit;\n-}\n-\n-static void create_object_directory(void)\n-{\n-\tstruct strbuf path = STRBUF_INIT;\n-\tsize_t baselen;\n-\n-\tstrbuf_addstr(&path, get_object_directory());\n-\tbaselen = path.len;\n-\n-\tsafe_create_dir(path.buf, 1);\n-\n-\tstrbuf_setlen(&path, baselen);\n-\tstrbuf_addstr(&path, \"/pack\");\n-\tsafe_create_dir(path.buf, 1);\n-\n-\tstrbuf_setlen(&path, baselen);\n-\tstrbuf_addstr(&path, \"/info\");\n-\tsafe_create_dir(path.buf, 1);\n-\n-\tstrbuf_release(&path);\n-}\n-\n-static void separate_git_dir(const char *git_dir, const char *git_link)\n-{\n-\tstruct stat st;\n-\n-\tif (!stat(git_link, &st)) {\n-\t\tconst char *src;\n-\n-\t\tif (S_ISREG(st.st_mode))\n-\t\t\tsrc = read_gitfile(git_link);\n-\t\telse if (S_ISDIR(st.st_mode))\n-\t\t\tsrc = git_link;\n-\t\telse\n-\t\t\tdie(_(\"unable to handle file type %d\"), (int)st.st_mode);\n-\n-\t\tif (rename(src, git_dir))\n-\t\t\tdie_errno(_(\"unable to move %s to %s\"), src, git_dir);\n-\t\trepair_worktrees(NULL, NULL);\n-\t}\n-\n-\twrite_file(git_link, \"gitdir: %s\", git_dir);\n-}\n-\n-static void validate_hash_algorithm(struct repository_format *repo_fmt, int hash)\n-{\n-\tconst char *env = getenv(GIT_DEFAULT_HASH_ENVIRONMENT);\n-\t/*\n-\t * If we already have an initialized repo, don't allow the user to\n-\t * specify a different algorithm, as that could cause corruption.\n-\t * Otherwise, if the user has specified one on the command line, use it.\n-\t */\n-\tif (repo_fmt->version >= 0 && hash != GIT_HASH_UNKNOWN && hash != repo_fmt->hash_algo)\n-\t\tdie(_(\"attempt to reinitialize repository with different hash\"));\n-\telse if (hash != GIT_HASH_UNKNOWN)\n-\t\trepo_fmt->hash_algo = hash;\n-\telse if (env) {\n-\t\tint env_algo = hash_algo_by_name(env);\n-\t\tif (env_algo == GIT_HASH_UNKNOWN)\n-\t\t\tdie(_(\"unknown hash algorithm '%s'\"), env);\n-\t\trepo_fmt->hash_algo = env_algo;\n-\t}\n-}\n-\n-int init_db(const char *git_dir, const char *real_git_dir,\n-\t    const char *template_dir, int hash, const char *initial_branch,\n-\t    unsigned int flags)\n-{\n-\tint reinit;\n-\tint exist_ok = flags & INIT_DB_EXIST_OK;\n-\tchar *original_git_dir = real_pathdup(git_dir, 1);\n-\tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n-\n-\tif (real_git_dir) {\n-\t\tstruct stat st;\n-\n-\t\tif (!exist_ok && !stat(git_dir, &st))\n-\t\t\tdie(_(\"%s already exists\"), git_dir);\n-\n-\t\tif (!exist_ok && !stat(real_git_dir, &st))\n-\t\t\tdie(_(\"%s already exists\"), real_git_dir);\n-\n-\t\tset_git_dir(real_git_dir, 1);\n-\t\tgit_dir = get_git_dir();\n-\t\tseparate_git_dir(git_dir, original_git_dir);\n-\t}\n-\telse {\n-\t\tset_git_dir(git_dir, 1);\n-\t\tgit_dir = get_git_dir();\n-\t}\n-\tstartup_info->have_repository = 1;\n-\n-\t/* Ensure `core.hidedotfiles` is processed */\n-\tgit_config(platform_core_config, NULL);\n-\n-\tsafe_create_dir(git_dir, 0);\n-\n-\tinit_is_bare_repository = is_bare_repository();\n-\n-\t/* Check to see if the repository version is right.\n-\t * Note that a newly created repository does not have\n-\t * config file, so this will not fail.  What we are catching\n-\t * is an attempt to reinitialize new repository with an old tool.\n-\t */\n-\tcheck_repository_format(&repo_fmt);\n-\n-\tvalidate_hash_algorithm(&repo_fmt, hash);\n-\n-\treinit = create_default_files(template_dir, original_git_dir,\n-\t\t\t\t      initial_branch, &repo_fmt,\n-\t\t\t\t      flags & INIT_DB_QUIET);\n-\tif (reinit && initial_branch)\n-\t\twarning(_(\"re-init: ignored --initial-branch=%s\"),\n-\t\t\tinitial_branch);\n-\n-\tcreate_object_directory();\n-\n-\tif (get_shared_repository()) {\n-\t\tchar buf[10];\n-\t\t/* We do not spell \"group\" and such, so that\n-\t\t * the configuration can be read by older version\n-\t\t * of git. Note, we use octal numbers for new share modes,\n-\t\t * and compatibility values for PERM_GROUP and\n-\t\t * PERM_EVERYBODY.\n-\t\t */\n-\t\tif (get_shared_repository() < 0)\n-\t\t\t/* force to the mode value */\n-\t\t\txsnprintf(buf, sizeof(buf), \"0%o\", -get_shared_repository());\n-\t\telse if (get_shared_repository() == PERM_GROUP)\n-\t\t\txsnprintf(buf, sizeof(buf), \"%d\", OLD_PERM_GROUP);\n-\t\telse if (get_shared_repository() == PERM_EVERYBODY)\n-\t\t\txsnprintf(buf, sizeof(buf), \"%d\", OLD_PERM_EVERYBODY);\n-\t\telse\n-\t\t\tBUG(\"invalid value for shared_repository\");\n-\t\tgit_config_set(\"core.sharedrepository\", buf);\n-\t\tgit_config_set(\"receive.denyNonFastforwards\", \"true\");\n-\t}\n-\n-\tif (!(flags & INIT_DB_QUIET)) {\n-\t\tint len = strlen(git_dir);\n-\n-\t\tif (reinit)\n-\t\t\tprintf(get_shared_repository()\n-\t\t\t       ? _(\"Reinitialized existing shared Git repository in %s%s\\n\")\n-\t\t\t       : _(\"Reinitialized existing Git repository in %s%s\\n\"),\n-\t\t\t       git_dir, len && git_dir[len-1] != '/' ? \"/\" : \"\");\n-\t\telse\n-\t\t\tprintf(get_shared_repository()\n-\t\t\t       ? _(\"Initialized empty shared Git repository in %s%s\\n\")\n-\t\t\t       : _(\"Initialized empty Git repository in %s%s\\n\"),\n-\t\t\t       git_dir, len && git_dir[len-1] != '/' ? \"/\" : \"\");\n-\t}\n-\n-\tfree(original_git_dir);\n-\treturn 0;\n-}\n-=======\n-#include \"worktree.h\"\n-#include \"wrapper.h\"\n-#include \"run-command.h\"\n-#include \"hook.h\"\n-\n-#ifdef NO_TRUSTABLE_FILEMODE\n-#define TEST_FILEMODE 0\n-#else\n-#define TEST_FILEMODE 1\n-#endif\n-\n-#define GIT_DEFAULT_HASH_ENVIRONMENT \"GIT_DEFAULT_HASH\"\n-\n-static int init_is_bare_repository = 0;\n-static int init_shared_repository = -1;\n-\n-static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,\n-\t\t\t     DIR *dir)\n-{\n-\tsize_t path_baselen = path->len;\n-\tsize_t template_baselen = template_path->len;\n-\tstruct dirent *de;\n-\tint is_hooks_dir = ends_with(template_path->buf, \"/hooks/\");\n-\n-\t/* Note: if \".git/hooks\" file exists in the repository being\n-\t * re-initialized, /etc/core-git/templates/hooks/update would\n-\t * cause \"git init\" to fail here.  I think this is sane but\n-\t * it means that the set of templates we ship by default, along\n-\t * with the way the namespace under .git/ is organized, should\n-\t * be really carefully chosen.\n-\t */\n-\tsafe_create_dir(path->buf, 1);\n-\twhile ((de = readdir(dir)) != NULL) {\n-\t\tstruct stat st_git, st_template;\n-\t\tint exists = 0;\n-\n-\t\tstrbuf_setlen(path, path_baselen);\n-\t\tstrbuf_setlen(template_path, template_baselen);\n-\n-\t\tif (de->d_name[0] == '.')\n-\t\t\tcontinue;\n-\t\tstrbuf_addstr(path, de->d_name);\n-\t\tstrbuf_addstr(template_path, de->d_name);\n-\t\tif (lstat(path->buf, &st_git)) {\n-\t\t\tif (errno != ENOENT)\n-\t\t\t\tdie_errno(_(\"cannot stat '%s'\"), path->buf);\n-\t\t}\n-\t\telse\n-\t\t\texists = 1;\n-\n-\t\tif (lstat(template_path->buf, &st_template))\n-\t\t\tdie_errno(_(\"cannot stat template '%s'\"), template_path->buf);\n-\n-\t\tif (S_ISDIR(st_template.st_mode)) {\n-\t\t\tDIR *subdir = opendir(template_path->buf);\n-\t\t\tif (!subdir)\n-\t\t\t\tdie_errno(_(\"cannot opendir '%s'\"), template_path->buf);\n-\t\t\tstrbuf_addch(path, '/');\n-\t\t\tstrbuf_addch(template_path, '/');\n-\t\t\tcopy_templates_1(path, template_path, subdir);\n-\t\t\tclosedir(subdir);\n-\t\t}\n-\t\telse if (exists)\n-\t\t\tcontinue;\n-\t\telse if (S_ISLNK(st_template.st_mode)) {\n-\t\t\tstruct strbuf lnk = STRBUF_INIT;\n-\t\t\tif (strbuf_readlink(&lnk, template_path->buf,\n-\t\t\t\t\t    st_template.st_size) < 0)\n-\t\t\t\tdie_errno(_(\"cannot readlink '%s'\"), template_path->buf);\n-\t\t\tif (symlink(lnk.buf, path->buf))\n-\t\t\t\tdie_errno(_(\"cannot symlink '%s' '%s'\"),\n-\t\t\t\t\t  lnk.buf, path->buf);\n-\t\t\tstrbuf_release(&lnk);\n-\t\t}\n-\t\telse if (S_ISREG(st_template.st_mode)) {\n-\t\t\tif (is_hooks_dir &&\n-\t\t\t    is_executable(template_path->buf))\n-\t\t\t\tadd_safe_hook(template_path->buf);\n-\n-\t\t\tif (copy_file(path->buf, template_path->buf, st_template.st_mode))\n-\t\t\t\tdie_errno(_(\"cannot copy '%s' to '%s'\"),\n-\t\t\t\t\t  template_path->buf, path->buf);\n-\t\t}\n-\t\telse\n-\t\t\terror(_(\"ignoring template %s\"), template_path->buf);\n-\t}\n-}\n-\n-static void copy_templates(const char *option_template)\n-{\n-\tconst char *template_dir = get_template_dir(option_template);\n-\tstruct strbuf path = STRBUF_INIT;\n-\tstruct strbuf template_path = STRBUF_INIT;\n-\tsize_t template_len;\n-\tstruct repository_format template_format = REPOSITORY_FORMAT_INIT;\n-\tstruct strbuf err = STRBUF_INIT;\n-\tDIR *dir;\n-\tchar *to_free = NULL;\n-\n-\tif (!template_dir || !*template_dir)\n-\t\treturn;\n-\n-\tstrbuf_addstr(&template_path, template_dir);\n-\tstrbuf_complete(&template_path, '/');\n-\ttemplate_len = template_path.len;\n-\n-\tdir = opendir(template_path.buf);\n-\tif (!dir) {\n-\t\twarning(_(\"templates not found in %s\"), template_dir);\n-\t\tgoto free_return;\n-\t}\n-\n-\t/* Make sure that template is from the correct vintage */\n-\tstrbuf_addstr(&template_path, \"config\");\n-\tread_repository_format(&template_format, template_path.buf);\n-\tstrbuf_setlen(&template_path, template_len);\n-\n-\t/*\n-\t * No mention of version at all is OK, but anything else should be\n-\t * verified.\n-\t */\n-\tif (template_format.version >= 0 &&\n-\t    verify_repository_format(&template_format, &err) < 0) {\n-\t\twarning(_(\"not copying templates from '%s': %s\"),\n-\t\t\t  template_dir, err.buf);\n-\t\tstrbuf_release(&err);\n-\t\tgoto close_free_return;\n-\t}\n-\n-\tstrbuf_addstr(&path, get_git_common_dir());\n-\tstrbuf_complete(&path, '/');\n-\tcopy_templates_1(&path, &template_path, dir);\n-close_free_return:\n-\tclosedir(dir);\n-free_return:\n-\tfree(to_free);\n-\tstrbuf_release(&path);\n-\tstrbuf_release(&template_path);\n-\tclear_repository_format(&template_format);\n-}\n-\n-/*\n- * If the git_dir is not directly inside the working tree, then git will not\n- * find it by default, and we need to set the worktree explicitly.\n- */\n-static int needs_work_tree_config(const char *git_dir, const char *work_tree)\n-{\n-\tif (!strcmp(work_tree, \"/\") && !strcmp(git_dir, \"/.git\"))\n-\t\treturn 0;\n-\tif (skip_prefix(git_dir, work_tree, &git_dir) &&\n-\t    !strcmp(git_dir, \"/.git\"))\n-\t\treturn 0;\n-\treturn 1;\n-}\n-\n-void initialize_repository_version(int hash_algo, int reinit)\n-{\n-\tchar repo_version_string[10];\n-\tint repo_version = GIT_REPO_VERSION;\n-\n-\tif (hash_algo != GIT_HASH_SHA1)\n-\t\trepo_version = GIT_REPO_VERSION_READ;\n-\n-\t/* This forces creation of new config file */\n-\txsnprintf(repo_version_string, sizeof(repo_version_string),\n-\t\t  \"%d\", repo_version);\n-\tgit_config_set(\"core.repositoryformatversion\", repo_version_string);\n-\n-\tif (hash_algo != GIT_HASH_SHA1)\n-\t\tgit_config_set(\"extensions.objectformat\",\n-\t\t\t       hash_algos[hash_algo].name);\n-\telse if (reinit)\n-\t\tgit_config_set_gently(\"extensions.objectformat\", NULL);\n-}\n-\n-static int create_default_files(const char *template_path,\n-\t\t\t\tconst char *original_git_dir,\n-\t\t\t\tconst char *initial_branch,\n-\t\t\t\tconst struct repository_format *fmt,\n-\t\t\t\tint quiet)\n-{\n-\tstruct stat st1;\n-\tstruct strbuf buf = STRBUF_INIT;\n-\tchar *path;\n-\tchar junk[2];\n-\tint reinit;\n-\tint filemode;\n-\tstruct strbuf err = STRBUF_INIT;\n-\tconst char *work_tree = get_git_work_tree();\n-\n-\t/*\n-\t * First copy the templates -- we might have the default\n-\t * config file there, in which case we would want to read\n-\t * from it after installing.\n-\t *\n-\t * Before reading that config, we also need to clear out any cached\n-\t * values (since we've just potentially changed what's available on\n-\t * disk).\n-\t */\n-\tcopy_templates(template_path);\n-\tgit_config_clear();\n-\treset_shared_repository();\n-\tgit_config(git_default_config, NULL);\n-\n-\t/*\n-\t * We must make sure command-line options continue to override any\n-\t * values we might have just re-read from the config.\n-\t */\n-\tis_bare_repository_cfg = init_is_bare_repository || !work_tree;\n-\tif (init_shared_repository != -1)\n-\t\tset_shared_repository(init_shared_repository);\n-\n-\t/*\n-\t * We would have created the above under user's umask -- under\n-\t * shared-repository settings, we would need to fix them up.\n-\t */\n-\tif (get_shared_repository()) {\n-\t\tadjust_shared_perm(get_git_dir());\n-\t}\n-\n-\t/*\n-\t * We need to create a \"refs\" dir in any case so that older\n-\t * versions of git can tell that this is a repository.\n-\t */\n-\tsafe_create_dir(git_path(\"refs\"), 1);\n-\tadjust_shared_perm(git_path(\"refs\"));\n-\n-\tif (refs_init_db(&err))\n-\t\tdie(\"failed to set up refs db: %s\", err.buf);\n-\n-\t/*\n-\t * Point the HEAD symref to the initial branch with if HEAD does\n-\t * not yet exist.\n-\t */\n-\tpath = git_path_buf(&buf, \"HEAD\");\n-\treinit = (!access(path, R_OK)\n-\t\t  || readlink(path, junk, sizeof(junk)-1) != -1);\n-\tif (!reinit) {\n-\t\tchar *ref;\n-\n-\t\tif (!initial_branch)\n-\t\t\tinitial_branch = git_default_branch_name(quiet);\n-\n-\t\tref = xstrfmt(\"refs/heads/%s\", initial_branch);\n-\t\tif (check_refname_format(ref, 0) < 0)\n-\t\t\tdie(_(\"invalid initial branch name: '%s'\"),\n-\t\t\t    initial_branch);\n-\n-\t\tif (create_symref(\"HEAD\", ref, NULL) < 0)\n-\t\t\texit(1);\n-\t\tfree(ref);\n-\t}\n-\n-\tinitialize_repository_version(fmt->hash_algo, 0);\n-\n-\t/* Check filemode trustability */\n-\tpath = git_path_buf(&buf, \"config\");\n-\tfilemode = TEST_FILEMODE;\n-\tif (TEST_FILEMODE && !lstat(path, &st1)) {\n-\t\tstruct stat st2;\n-\t\tfilemode = (!chmod(path, st1.st_mode ^ S_IXUSR) &&\n-\t\t\t\t!lstat(path, &st2) &&\n-\t\t\t\tst1.st_mode != st2.st_mode &&\n-\t\t\t\t!chmod(path, st1.st_mode));\n-\t\tif (filemode && !reinit && (st1.st_mode & S_IXUSR))\n-\t\t\tfilemode = 0;\n-\t}\n-\tgit_config_set(\"core.filemode\", filemode ? \"true\" : \"false\");\n-\n-\tif (is_bare_repository())\n-\t\tgit_config_set(\"core.bare\", \"true\");\n-\telse {\n-\t\tgit_config_set(\"core.bare\", \"false\");\n-\t\t/* allow template config file to override the default */\n-\t\tif (log_all_ref_updates == LOG_REFS_UNSET)\n-\t\t\tgit_config_set(\"core.logallrefupdates\", \"true\");\n-\t\tif (needs_work_tree_config(original_git_dir, work_tree))\n-\t\t\tgit_config_set(\"core.worktree\", work_tree);\n-\t}\n-\n-\tif (!reinit) {\n-\t\t/* Check if symlink is supported in the work tree */\n-\t\tpath = git_path_buf(&buf, \"tXXXXXX\");\n-\t\tif (!close(xmkstemp(path)) &&\n-\t\t    !unlink(path) &&\n-\t\t    !symlink(\"testing\", path) &&\n-\t\t    !lstat(path, &st1) &&\n-\t\t    S_ISLNK(st1.st_mode))\n-\t\t\tunlink(path); /* good */\n-\t\telse\n-\t\t\tgit_config_set(\"core.symlinks\", \"false\");\n-\n-\t\t/* Check if the filesystem is case-insensitive */\n-\t\tpath = git_path_buf(&buf, \"CoNfIg\");\n-\t\tif (!access(path, F_OK))\n-\t\t\tgit_config_set(\"core.ignorecase\", \"true\");\n-\t\tprobe_utf8_pathname_composition();\n-\t}\n-\n-\tstrbuf_release(&buf);\n-\treturn reinit;\n-}\n-\n-static void create_object_directory(void)\n-{\n-\tstruct strbuf path = STRBUF_INIT;\n-\tsize_t baselen;\n-\n-\tstrbuf_addstr(&path, get_object_directory());\n-\tbaselen = path.len;\n-\n-\tsafe_create_dir(path.buf, 1);\n-\n-\tstrbuf_setlen(&path, baselen);\n-\tstrbuf_addstr(&path, \"/pack\");\n-\tsafe_create_dir(path.buf, 1);\n-\n-\tstrbuf_setlen(&path, baselen);\n-\tstrbuf_addstr(&path, \"/info\");\n-\tsafe_create_dir(path.buf, 1);\n-\n-\tstrbuf_release(&path);\n-}\n-\n-static void separate_git_dir(const char *git_dir, const char *git_link)\n-{\n-\tstruct stat st;\n-\n-\tif (!stat(git_link, &st)) {\n-\t\tconst char *src;\n-\n-\t\tif (S_ISREG(st.st_mode))\n-\t\t\tsrc = read_gitfile(git_link);\n-\t\telse if (S_ISDIR(st.st_mode))\n-\t\t\tsrc = git_link;\n-\t\telse\n-\t\t\tdie(_(\"unable to handle file type %d\"), (int)st.st_mode);\n-\n-\t\tif (rename(src, git_dir))\n-\t\t\tdie_errno(_(\"unable to move %s to %s\"), src, git_dir);\n-\t\trepair_worktrees(NULL, NULL);\n-\t}\n-\n-\twrite_file(git_link, \"gitdir: %s\", git_dir);\n-}\n-\n-static void validate_hash_algorithm(struct repository_format *repo_fmt, int hash)\n-{\n-\tconst char *env = getenv(GIT_DEFAULT_HASH_ENVIRONMENT);\n-\t/*\n-\t * If we already have an initialized repo, don't allow the user to\n-\t * specify a different algorithm, as that could cause corruption.\n-\t * Otherwise, if the user has specified one on the command line, use it.\n-\t */\n-\tif (repo_fmt->version >= 0 && hash != GIT_HASH_UNKNOWN && hash != repo_fmt->hash_algo)\n-\t\tdie(_(\"attempt to reinitialize repository with different hash\"));\n-\telse if (hash != GIT_HASH_UNKNOWN)\n-\t\trepo_fmt->hash_algo = hash;\n-\telse if (env) {\n-\t\tint env_algo = hash_algo_by_name(env);\n-\t\tif (env_algo == GIT_HASH_UNKNOWN)\n-\t\t\tdie(_(\"unknown hash algorithm '%s'\"), env);\n-\t\trepo_fmt->hash_algo = env_algo;\n-\t}\n-}\n-\n-int init_db(const char *git_dir, const char *real_git_dir,\n-\t    const char *template_dir, int hash, const char *initial_branch,\n-\t    unsigned int flags)\n-{\n-\tint reinit;\n-\tint exist_ok = flags & INIT_DB_EXIST_OK;\n-\tchar *original_git_dir = real_pathdup(git_dir, 1);\n-\tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n-\n-\tif (real_git_dir) {\n-\t\tstruct stat st;\n-\n-\t\tif (!exist_ok && !stat(git_dir, &st))\n-\t\t\tdie(_(\"%s already exists\"), git_dir);\n-\n-\t\tif (!exist_ok && !stat(real_git_dir, &st))\n-\t\t\tdie(_(\"%s already exists\"), real_git_dir);\n-\n-\t\tset_git_dir(real_git_dir, 1);\n-\t\tgit_dir = get_git_dir();\n-\t\tseparate_git_dir(git_dir, original_git_dir);\n-\t}\n-\telse {\n-\t\tset_git_dir(git_dir, 1);\n-\t\tgit_dir = get_git_dir();\n-\t}\n-\tstartup_info->have_repository = 1;\n-\n-\t/* Ensure `core.hidedotfiles` is processed */\n-\tgit_config(platform_core_config, NULL);\n-\n-\tsafe_create_dir(git_dir, 0);\n-\n-\tinit_is_bare_repository = is_bare_repository();\n-\n-\t/* Check to see if the repository version is right.\n-\t * Note that a newly created repository does not have\n-\t * config file, so this will not fail.  What we are catching\n-\t * is an attempt to reinitialize new repository with an old tool.\n-\t */\n-\tcheck_repository_format(&repo_fmt);\n-\n-\tvalidate_hash_algorithm(&repo_fmt, hash);\n-\n-\treinit = create_default_files(template_dir, original_git_dir,\n-\t\t\t\t      initial_branch, &repo_fmt,\n-\t\t\t\t      flags & INIT_DB_QUIET);\n-\tif (reinit && initial_branch)\n-\t\twarning(_(\"re-init: ignored --initial-branch=%s\"),\n-\t\t\tinitial_branch);\n-\n-\tcreate_object_directory();\n-\n-\tif (get_shared_repository()) {\n-\t\tchar buf[10];\n-\t\t/* We do not spell \"group\" and such, so that\n-\t\t * the configuration can be read by older version\n-\t\t * of git. Note, we use octal numbers for new share modes,\n-\t\t * and compatibility values for PERM_GROUP and\n-\t\t * PERM_EVERYBODY.\n-\t\t */\n-\t\tif (get_shared_repository() < 0)\n-\t\t\t/* force to the mode value */\n-\t\t\txsnprintf(buf, sizeof(buf), \"0%o\", -get_shared_repository());\n-\t\telse if (get_shared_repository() == PERM_GROUP)\n-\t\t\txsnprintf(buf, sizeof(buf), \"%d\", OLD_PERM_GROUP);\n-\t\telse if (get_shared_repository() == PERM_EVERYBODY)\n-\t\t\txsnprintf(buf, sizeof(buf), \"%d\", OLD_PERM_EVERYBODY);\n-\t\telse\n-\t\t\tBUG(\"invalid value for shared_repository\");\n-\t\tgit_config_set(\"core.sharedrepository\", buf);\n-\t\tgit_config_set(\"receive.denyNonFastforwards\", \"true\");\n-\t}\n-\n-\tif (!(flags & INIT_DB_QUIET)) {\n-\t\tint len = strlen(git_dir);\n-\n-\t\tif (reinit)\n-\t\t\tprintf(get_shared_repository()\n-\t\t\t       ? _(\"Reinitialized existing shared Git repository in %s%s\\n\")\n-\t\t\t       : _(\"Reinitialized existing Git repository in %s%s\\n\"),\n-\t\t\t       git_dir, len && git_dir[len-1] != '/' ? \"/\" : \"\");\n-\t\telse\n-\t\t\tprintf(get_shared_repository()\n-\t\t\t       ? _(\"Initialized empty shared Git repository in %s%s\\n\")\n-\t\t\t       : _(\"Initialized empty Git repository in %s%s\\n\"),\n-\t\t\t       git_dir, len && git_dir[len-1] != '/' ? \"/\" : \"\");\n-\t}\n-\n-\tfree(original_git_dir);\n-\treturn 0;\n-}\n->>>>>>> c7b6b0a28f (Merge branch 'js/fix-clone-w-hooks-2.40' into HEAD)\n \n static int guess_repository_type(const char *git_dir)\n {\ndiff --git a/config.c b/config.c\nremerge CONFLICT (content): Merge conflict in config.c\nindex f0f09004c8..3846a37be9 100644\n--- a/config.c\n+++ b/config.c\n@@ -1558,33 +1558,7 @@ static int git_default_core_config(const char *var, const char *value,\n \tif (!strcmp(var, \"core.attributesfile\"))\n \t\treturn git_config_pathname(&git_attributes_file, var, value);\n \n-<<<<<<< babb4e5d71 (Git 2.42.2)\n-\tif (!strcmp(var, \"core.hookspath\")) {\n-\t\tif (ctx->kvi && ctx->kvi->scope == CONFIG_SCOPE_LOCAL &&\n-\t\t    git_env_bool(\"GIT_CLONE_PROTECTION_ACTIVE\", 0))\n-\t\t\tdie(_(\"active `core.hooksPath` found in the local \"\n-\t\t\t      \"repository config:\\n\\t%s\\nFor security \"\n-\t\t\t      \"reasons, this is disallowed by default.\\nIf \"\n-\t\t\t      \"this is intentional and the hook should \"\n-\t\t\t      \"actually be run, please\\nrun the command \"\n-\t\t\t      \"again with \"\n-\t\t\t      \"`GIT_CLONE_PROTECTION_ACTIVE=false`\"),\n-\t\t\t    value);\n-||||||| 0f15832059\n-\tif (!strcmp(var, \"core.hookspath\")) {\n-\t\tif (current_config_scope() == CONFIG_SCOPE_LOCAL &&\n-\t\t    git_env_bool(\"GIT_CLONE_PROTECTION_ACTIVE\", 0))\n-\t\t\tdie(_(\"active `core.hooksPath` found in the local \"\n-\t\t\t      \"repository config:\\n\\t%s\\nFor security \"\n-\t\t\t      \"reasons, this is disallowed by default.\\nIf \"\n-\t\t\t      \"this is intentional and the hook should \"\n-\t\t\t      \"actually be run, please\\nrun the command \"\n-\t\t\t      \"again with \"\n-\t\t\t      \"`GIT_CLONE_PROTECTION_ACTIVE=false`\"),\n-\t\t\t    value);\n-=======\n \tif (!strcmp(var, \"core.hookspath\"))\n->>>>>>> c7b6b0a28f (Merge branch 'js/fix-clone-w-hooks-2.40' into HEAD)\n \t\treturn git_config_pathname(&git_hooks_path, var, value);\n \n \tif (!strcmp(var, \"core.bare\")) {\ndiff --git a/hook.c b/hook.c\nindex 7ddb677216..7be471e335 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -59,7 +59,8 @@ void add_safe_hook(const char *path)\n \t}\n }\n \n-static int safe_hook_cb(const char *key, const char *value, void *d)\n+static int safe_hook_cb(const char *key, const char *value,\n+\t\t\tconst struct config_context *ctx UNUSED, void *d)\n {\n \tstruct strset *set = d;\n \ndiff --git a/setup.c b/setup.c\nindex ea2aec5aaa..dc4897543e 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -18,6 +18,7 @@\n #include \"trace2.h\"\n #include \"worktree.h\"\n #include \"exec-cmd.h\"\n+#include \"run-command.h\"\n #include \"hook.h\"\n \n static int inside_git_dir = -1;\n@@ -1794,6 +1795,7 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,\n \tsize_t path_baselen = path->len;\n \tsize_t template_baselen = template_path->len;\n \tstruct dirent *de;\n+\tint is_hooks_dir = ends_with(template_path->buf, \"/hooks/\");\n \n \t/* Note: if \".git/hooks\" file exists in the repository being\n \t * re-initialized, /etc/core-git/templates/hooks/update would\n@@ -1846,6 +1848,10 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,\n \t\t\tstrbuf_release(&lnk);\n \t\t}\n \t\telse if (S_ISREG(st_template.st_mode)) {\n+\t\t\tif (is_hooks_dir &&\n+\t\t\t    is_executable(template_path->buf))\n+\t\t\t\tadd_safe_hook(template_path->buf);\n+\n \t\t\tif (copy_file(path->buf, template_path->buf, st_template.st_mode))\n \t\t\t\tdie_errno(_(\"cannot copy '%s' to '%s'\"),\n \t\t\t\t\t  template_path->buf, path->buf);\n* js/fix-clone-w-hooks-2.43\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nremerge CONFLICT (content): Merge conflict in t/t1800-hook.sh\nindex 1283a9bd47..c51be5f7a0 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -191,24 +191,7 @@ test_expect_success '`safe.hook.sha256` and clone protections' '\n \techo \"called hook\" >safe-hook.log\n \tEOF\n \n-<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 1f2e64e22d (Git 2.43.4)\n-\tgit hook run test-hook 2>err &&\n-\ttest_grep \"Hook ran\" err &&\n-|||||||||||||||||||||||||||||||| babb4e5d71\n-\tgit hook run test-hook 2>err &&\n-\tgrep \"Hook ran\" err &&\n-================================\n->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 0268cf79cb (Merge branch 'js/fix-clone-w-hooks-2.41' into HEAD)\n \ttest_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \\\n-<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 1f2e64e22d (Git 2.43.4)\n-\t\tgit hook run test-hook 2>err &&\n-\ttest_grep \"active .core.hooksPath\" err &&\n-\ttest_grep ! \"Hook ran\" err\n-|||||||||||||||||||||||||||||||| babb4e5d71\n-\t\tgit hook run test-hook 2>err &&\n-\tgrep \"active .core.hooksPath\" err &&\n-\t! grep \"Hook ran\" err\n-================================\n \t\tgit -C safe-hook hook run pre-push 2>err &&\n \tcmd=\"$(grep \"git config --global --add safe.hook.sha256 [0-9a-f]\" err)\" &&\n \teval \"$cmd\" &&\n@@ -235,7 +218,6 @@ test_expect_success 'Git LFS special-handling in clone protections' '\n \t\tgit -C lfs-hooks hook run pre-push &&\n \ttest_write_lines \"called pre-push\" >expect &&\n \ttest_cmp lfs-hooks/fake-git-lfs.log expect\n->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 0268cf79cb (Merge branch 'js/fix-clone-w-hooks-2.41' into HEAD)\n '\n \n test_done\ndiff --git a/t/t5601-clone.sh b/t/t5601-clone.sh\nindex 6ee833bac2..0fcfe6f2ce 100755\n--- a/t/t5601-clone.sh\n+++ b/t/t5601-clone.sh\n@@ -835,7 +835,7 @@ test_expect_success 'clone with init.templatedir runs hooks' '\n \t\tgit -c init.templateDir=\"$(pwd)/tmpl\" \\\n \t\t\tclone --recurse-submodules \\\n \t\t\ttmpl/hooks hook-run-submodule 2>err &&\n-\t\t! grep \"active .* hook found\" err &&\n+\t\ttest_grep ! \"active .* hook found\" err &&\n \t\ttest_path_is_file hook-run-submodule/hook.run &&\n \t\ttest_path_is_file hook-run-submodule/sub/hook.run\n \t)\n* js/fix-clone-w-hooks-2.44* js/fix-clone-w-hooks-2.45\n"},{"id":"495037","messageId":"20240518181432.GA1570600@coredump.intra.peff.net","threadId":"61509","inReplyTo":"b841db8392ebd924d1893829a7e5e22240f1e9cf.1716028366.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v2 5/8] hook(clone protections): add escape hatch","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-18T18:14:32Z","receivedAt":"2024-05-18T18:14:41Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Sat, May 18, 2024 at 10:32:43AM +0000, Johannes Schindelin via GitGitGadget wrote:\n\n> To help Git LFS, and other tools behaving similarly (if there are any),\n> let's add a new, multi-valued `safe.hook.sha256` config setting. Like\n> the already-existing `safe.*` settings, it is ignored in\n> repository-local configs, and it is interpreted as a list of SHA-256\n> checksums of hooks' contents that are safe to execute during a clone\n> operation. Future Git LFS versions will need to write those entries at\n> the same time they install the `smudge`/`clean` filters.\n\nThis scheme seems more complicated for the user than the sometimes\ndiscussed ability to specify hook paths via config (not core.hooksPath,\nwhich covers _all_ hooks, but one which allows a per-hook path).\n\nIn either case, we're considering config to be a trusted source of\ntruth, so I think the security properties are the same. But for the\nsystem here, a user updating a hook needs to do multiple steps:\n\n  - compute the sha256 of the hook (for which we provide no tooling\n    support, though hopefully it is obvious how to use other tools)\n\n  - add the config for the sha256\n\n  - install the new hook into $GIT_DIR/hooks\n\nWhereas if the config can just point at the hook, then there is only one\nstep: add the config for the hook (presumably pointing to a system\nversion that would have been copied into $GIT_DIR/hooks previously).\n\nLikewise for updates of the hooks, where the sha256 scheme requires\ncomputing and adding a new hash. But when the config just points to the\npath, there is no additional step for updating.\n\nIn either scheme, programs like git-lfs would have to adjust to the new\nworld view. The main advantage of the sha256 scheme, it seems to me, is\nthat the baked-in sha256 values let existing versions of git-lfs work.\nBut we could also support that internally, without exposing\nsafe.hook.sha256 to the world (and thus creating an ecosystem where we\nhave to support it forever).\n\nImplied here is that I also think config-based hooks have a lot of\n_other_ advantages, and so would be worth pursuing anyway, and this\nextra safety would come along for free. I won't enumerate those\nadvantages here, but we that can be a separate discussion if need be.\n\nAnd of course that feature doesn't yet exist, and is a much larger one.\nBut besides un-breaking current LFS, I'm not sure that we need to rush\nout a more generic version of the feature.\n\n-Peff\n"},{"id":"495038","messageId":"xmqqmsonne6m.fsf@gitster.g","threadId":"61509","inReplyTo":"20240518181432.GA1570600@coredump.intra.peff.net","subject":"Re: [PATCH v2 5/8] hook(clone protections): add escape hatch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-18T18:54:41Z","receivedAt":"2024-05-18T18:54:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> In either case, we're considering config to be a trusted source of\n> truth, so I think the security properties are the same. But for the\n> system here, a user updating a hook needs to do multiple steps:\n>\n>   - compute the sha256 of the hook (for which we provide no tooling\n>     support, though hopefully it is obvious how to use other tools)\n>\n>   - add the config for the sha256\n>\n>   - install the new hook into $GIT_DIR/hooks\n\nI am not sure why any of the above is needed.  \n\nHmph.\n\nI was somehow (because that is how \"git config --help\" explains\n\"safe.hook.*\") led to believe that this \"safety\" was only about \"git\nclone would prefer not to run ANY hook before it finishes operation\nand gives back the control to the end user, but historically it ran\nany enabled hooks in the resulting repository that was freshly\ncreated by it---so let's at least make sure the contents of the\nhooks are known-to-be-good ones when 'git clone' runs the hooks\".\nMost importantly, once \"git clone\" gives control back to the end\nuser and the end user had a chance to inspect the resulting\nrepository, the files in $GIT_DIR/hooks can be updated and the hooks\nwill run without incurring any cost of checking.\n\nIsn't that what happens?\n\nLooking at the control flow, hook.c:find_hook() is the one that\ncalls the function is_hook_safe_during_clone() to reject \"unsafe\"\nones (and allow the white-listed ones), but I do not know offhand\nhow the code limits the rejection only during clone.  So perhaps\nthis set of patches need further work to restrict the checks only to\n\"while we are cloning\" case?\n\n\n"},{"id":"495039","messageId":"89e16ab9-1ddc-d88f-41aa-c0c6eabe779c@gmx.de","threadId":"61509","inReplyTo":"xmqq4jawdlpa.fsf@gitster.g","subject":"Re: [PATCH 4/8] tests: verify that `clone -c core.hooksPath=/dev/null` works again","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2024-05-18T18:58:16Z","receivedAt":"2024-05-18T18:58:23Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Junio,\n\nOn Fri, 17 May 2024, Junio C Hamano wrote:\n\n> \"Johannes Schindelin via GitGitGadget\" <gitgitgadget@gmail.com>\n> writes:\n>\n> > What the added protection did not anticipate is that such a\n> > repository-local `core.hooksPath` can not only be used to point to\n> > maliciously-placed scripts in the current worktree, but also to\n> > _prevent_ hooks from being called altogether.\n> > ...\n> > diff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh\n> > index f6dc83e2aab..1eae346a6e3 100755\n> > --- a/t/t1350-config-hooks-path.sh\n> > +++ b/t/t1350-config-hooks-path.sh\n> > @@ -41,4 +41,8 @@ test_expect_success 'git rev-parse --git-path hooks' '\n> >  \ttest .git/custom-hooks/abc = \"$(cat actual)\"\n> >  '\n> >\n> > +test_expect_success 'core.hooksPath=/dev/null' '\n> > +\tgit clone -c core.hooksPath=/dev/null . no-templates\n> > +'\n>\n> Is it sufficient that the command exits with 0?  I am wondering if\n> we want to verify that the resulting repository looks like it\n> should, e.g., with\n>\n>     v=$(git -C no-templates config --local --get core.hookspath) &&\n>     test \"$v\" = /dev/null\n>\n> or something silly like that.\n\nI've added that, but would like to stress that the regression was _not_\nthat the `core.hooksPath` setting was missing from the local config. I've\nadded it because the implied suggestion is valid that we'll want to ensure\nthat the test case passes for the _correct_ reason ;-)\n\nCiao,\nJohannes\n"},{"id":"495041","messageId":"c67780c5-ab50-37f0-a507-91e8b721b349@gmx.de","threadId":"61509","inReplyTo":"xmqqy187nj4u.fsf@gitster.g","subject":"Re: [PATCH v2 0/8] Various fixes for v2.45.1 and friends","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2024-05-18T19:22:18Z","receivedAt":"2024-05-18T19:22:32Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Junio,\n\nOn Sat, 18 May 2024, Junio C Hamano wrote:\n\n> I have applied this to maint-2.39 and then merged them up to the\n> maintenance tracks.  The results will be pushed out to the \"split\n> out\" repository at\n>\n>     https://github.com/gitster/git/\n>\n> as these branches:\n>\n>     js/fix-clone-w-hooks-2.39\n>     js/fix-clone-w-hooks-2.40\n>     js/fix-clone-w-hooks-2.41\n>     js/fix-clone-w-hooks-2.42\n>     js/fix-clone-w-hooks-2.43\n>     js/fix-clone-w-hooks-2.44\n>     js/fix-clone-w-hooks-2.45\n\nThank you!\n\nAs it happens, I had already worked on tentative/maint-* branches (that\nyou can see here: https://github.com/dscho/git/branches/active), but\nhad to take a break before finalizing them.\n\nThe major difference I see is that js/fix-clone-w-hooks-2.41 still\ndeclares `do_files_match()` in `setup.h` (even if it is no longer defined\nor called), and that `hook.c` includes `copy.h` (but that is no longer\nneeded), and that `setup.h` includes `hook.h` (but that's not needed\neither).\n\nWhile comparing, I noticed that I had missed an extra empty line in my\nmerge conflict resolutions, a `grep` -> `test_grep` conversation and an\n`UNUSED` attribute for the `ctx` parameter of the `safe_hook_cb()`\nfunction. So I am really grateful that you did those integrations\nindependently.\n\nYou will also note that I tentatively added commits to mark v2.39.5, ...,\nv2.45.2 ready for tagging, along with release notes, just so we can hit\nthe ground running as soon as reviews settle down.\n\nCiao,\nJohannes\n\n>\n> For those who are merging them up at home yourselves, here are the\n> remerge diff that show the conflict resolution I made.\n>\n> * js/fix-clone-w-hooks-2.40\n> diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\n> remerge CONFLICT (content): Merge conflict in t/t1800-hook.sh\n> index 2af93d130d..bc0ed30ab9 100755\n> --- a/t/t1800-hook.sh\n> +++ b/t/t1800-hook.sh\n> @@ -177,7 +177,6 @@ test_expect_success 'git hook run a hook with a bad shebang' '\n>  \ttest_cmp expect actual\n>  '\n>\n> -<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< b9b439e0e3 (Git 2.40.2)\n>  test_expect_success 'stdin to hooks' '\n>  \twrite_script .git/hooks/test-hook <<-\\EOF &&\n>  \techo BEGIN stdin\n> @@ -196,23 +195,10 @@ test_expect_success 'stdin to hooks' '\n>  \ttest_cmp expect actual\n>  '\n>\n> -test_expect_success 'clone protections' '\n> -\ttest_config core.hooksPath \"$(pwd)/my-hooks\" &&\n> -\tmkdir -p my-hooks &&\n> -\twrite_script my-hooks/test-hook <<-\\EOF &&\n> -\techo Hook ran $1\n> -|||||||||||||||||||||||||||||||| 47b6d90e91\n> -test_expect_success 'clone protections' '\n> -\ttest_config core.hooksPath \"$(pwd)/my-hooks\" &&\n> -\tmkdir -p my-hooks &&\n> -\twrite_script my-hooks/test-hook <<-\\EOF &&\n> -\techo Hook ran $1\n> -================================\n>  test_expect_success '`safe.hook.sha256` and clone protections' '\n>  \tgit init safe-hook &&\n>  \twrite_script safe-hook/.git/hooks/pre-push <<-\\EOF &&\n>  \techo \"called hook\" >safe-hook.log\n> ->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 851218a8af (Revert \"Add a helper function to compare file contents\")\n>  \tEOF\n>\n>  \ttest_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \\\n> * js/fix-clone-w-hooks-2.41\n> diff --git a/builtin/init-db.c b/builtin/init-db.c\n> remerge CONFLICT (content): Merge conflict in builtin/init-db.c\n> index 0f0d2033c5..b351fe6e40 100644\n> --- a/builtin/init-db.c\n> +++ b/builtin/init-db.c\n> @@ -17,13 +17,9 @@\n>  #include \"path.h\"\n>  #include \"setup.h\"\n>  #include \"worktree.h\"\n> -<<<<<<< 0f15832059 (Git 2.41.1)\n>  #include \"wrapper.h\"\n> -||||||| b9b439e0e3\n> -=======\n>  #include \"run-command.h\"\n>  #include \"hook.h\"\n> ->>>>>>> 752e921355 (Merge branch 'js/fix-clone-w-hooks-2.39' into HEAD)\n>\n>  #ifdef NO_TRUSTABLE_FILEMODE\n>  #define TEST_FILEMODE 0\n> diff --git a/cache.h b/cache.h\n> remerge CONFLICT (content): Merge conflict in cache.h\n> index d99735c623..bdedb87e83 100644\n> --- a/cache.h\n> +++ b/cache.h\n> @@ -555,1301 +555,7 @@ extern int verify_ce_order;\n>  #define DATA_CHANGED    0x0020\n>  #define TYPE_CHANGED    0x0040\n>\n> -<<<<<<< 0f15832059 (Git 2.41.1)\n>  int cmp_cache_name_compare(const void *a_, const void *b_);\n> -||||||| b9b439e0e3\n> -/*\n> - * Return an abbreviated sha1 unique within this repository's object database.\n> - * The result will be at least `len` characters long, and will be NUL\n> - * terminated.\n> - *\n> - * The non-`_r` version returns a static buffer which remains valid until 4\n> - * more calls to find_unique_abbrev are made.\n> - *\n> - * The `_r` variant writes to a buffer supplied by the caller, which must be at\n> - * least `GIT_MAX_HEXSZ + 1` bytes. The return value is the number of bytes\n> - * written (excluding the NUL terminator).\n> - *\n> - * Note that while this version avoids the static buffer, it is not fully\n> - * reentrant, as it calls into other non-reentrant git code.\n> - */\n> -const char *repo_find_unique_abbrev(struct repository *r, const struct object_id *oid, int len);\n> -#define find_unique_abbrev(oid, len) repo_find_unique_abbrev(the_repository, oid, len)\n> -int repo_find_unique_abbrev_r(struct repository *r, char *hex, const struct object_id *oid, int len);\n> -#define find_unique_abbrev_r(hex, oid, len) repo_find_unique_abbrev_r(the_repository, hex, oid, len)\n> -\n> -/* set default permissions by passing mode arguments to open(2) */\n> -int git_mkstemps_mode(char *pattern, int suffix_len, int mode);\n> -int git_mkstemp_mode(char *pattern, int mode);\n> -\n> -/*\n> - * NOTE NOTE NOTE!!\n> - *\n> - * PERM_UMASK, OLD_PERM_GROUP and OLD_PERM_EVERYBODY enumerations must\n> - * not be changed. Old repositories have core.sharedrepository written in\n> - * numeric format, and therefore these values are preserved for compatibility\n> - * reasons.\n> - */\n> -enum sharedrepo {\n> -\tPERM_UMASK          = 0,\n> -\tOLD_PERM_GROUP      = 1,\n> -\tOLD_PERM_EVERYBODY  = 2,\n> -\tPERM_GROUP          = 0660,\n> -\tPERM_EVERYBODY      = 0664\n> -};\n> -int git_config_perm(const char *var, const char *value);\n> -int adjust_shared_perm(const char *path);\n> -\n> -/*\n> - * Create the directory containing the named path, using care to be\n> - * somewhat safe against races. Return one of the scld_error values to\n> - * indicate success/failure. On error, set errno to describe the\n> - * problem.\n> - *\n> - * SCLD_VANISHED indicates that one of the ancestor directories of the\n> - * path existed at one point during the function call and then\n> - * suddenly vanished, probably because another process pruned the\n> - * directory while we were working.  To be robust against this kind of\n> - * race, callers might want to try invoking the function again when it\n> - * returns SCLD_VANISHED.\n> - *\n> - * safe_create_leading_directories() temporarily changes path while it\n> - * is working but restores it before returning.\n> - * safe_create_leading_directories_const() doesn't modify path, even\n> - * temporarily. Both these variants adjust the permissions of the\n> - * created directories to honor core.sharedRepository, so they are best\n> - * suited for files inside the git dir. For working tree files, use\n> - * safe_create_leading_directories_no_share() instead, as it ignores\n> - * the core.sharedRepository setting.\n> - */\n> -enum scld_error {\n> -\tSCLD_OK = 0,\n> -\tSCLD_FAILED = -1,\n> -\tSCLD_PERMS = -2,\n> -\tSCLD_EXISTS = -3,\n> -\tSCLD_VANISHED = -4\n> -};\n> -enum scld_error safe_create_leading_directories(char *path);\n> -enum scld_error safe_create_leading_directories_const(const char *path);\n> -enum scld_error safe_create_leading_directories_no_share(char *path);\n> -\n> -int mkdir_in_gitdir(const char *path);\n> -char *interpolate_path(const char *path, int real_home);\n> -/* NEEDSWORK: remove this synonym once in-flight topics have migrated */\n> -#define expand_user_path interpolate_path\n> -const char *enter_repo(const char *path, int strict);\n> -static inline int is_absolute_path(const char *path)\n> -{\n> -\treturn is_dir_sep(path[0]) || has_dos_drive_prefix(path);\n> -}\n> -int is_directory(const char *);\n> -char *strbuf_realpath(struct strbuf *resolved, const char *path,\n> -\t\t      int die_on_error);\n> -char *strbuf_realpath_forgiving(struct strbuf *resolved, const char *path,\n> -\t\t\t\tint die_on_error);\n> -char *real_pathdup(const char *path, int die_on_error);\n> -const char *absolute_path(const char *path);\n> -char *absolute_pathdup(const char *path);\n> -const char *remove_leading_path(const char *in, const char *prefix);\n> -const char *relative_path(const char *in, const char *prefix, struct strbuf *sb);\n> -int normalize_path_copy_len(char *dst, const char *src, int *prefix_len);\n> -int normalize_path_copy(char *dst, const char *src);\n> -int longest_ancestor_length(const char *path, struct string_list *prefixes);\n> -char *strip_path_suffix(const char *path, const char *suffix);\n> -int daemon_avoid_alias(const char *path);\n> -\n> -/*\n> - * These functions match their is_hfs_dotgit() counterparts; see utf8.h for\n> - * details.\n> - */\n> -int is_ntfs_dotgit(const char *name);\n> -int is_ntfs_dotgitmodules(const char *name);\n> -int is_ntfs_dotgitignore(const char *name);\n> -int is_ntfs_dotgitattributes(const char *name);\n> -int is_ntfs_dotmailmap(const char *name);\n> -\n> -/*\n> - * Returns true iff \"str\" could be confused as a command-line option when\n> - * passed to a sub-program like \"ssh\". Note that this has nothing to do with\n> - * shell-quoting, which should be handled separately; we're assuming here that\n> - * the string makes it verbatim to the sub-program.\n> - */\n> -int looks_like_command_line_option(const char *str);\n> -\n> -/**\n> - * Return a newly allocated string with the evaluation of\n> - * \"$XDG_CONFIG_HOME/$subdir/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n> - * \"$HOME/.config/$subdir/$filename\". Return NULL upon error.\n> - */\n> -char *xdg_config_home_for(const char *subdir, const char *filename);\n> -\n> -/**\n> - * Return a newly allocated string with the evaluation of\n> - * \"$XDG_CONFIG_HOME/git/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n> - * \"$HOME/.config/git/$filename\". Return NULL upon error.\n> - */\n> -char *xdg_config_home(const char *filename);\n> -\n> -/**\n> - * Return a newly allocated string with the evaluation of\n> - * \"$XDG_CACHE_HOME/git/$filename\" if $XDG_CACHE_HOME is non-empty, otherwise\n> - * \"$HOME/.cache/git/$filename\". Return NULL upon error.\n> - */\n> -char *xdg_cache_home(const char *filename);\n> -\n> -int git_open_cloexec(const char *name, int flags);\n> -#define git_open(name) git_open_cloexec(name, O_RDONLY)\n> -\n> -/**\n> - * unpack_loose_header() initializes the data stream needed to unpack\n> - * a loose object header.\n> - *\n> - * Returns:\n> - *\n> - * - ULHR_OK on success\n> - * - ULHR_BAD on error\n> - * - ULHR_TOO_LONG if the header was too long\n> - *\n> - * It will only parse up to MAX_HEADER_LEN bytes unless an optional\n> - * \"hdrbuf\" argument is non-NULL. This is intended for use with\n> - * OBJECT_INFO_ALLOW_UNKNOWN_TYPE to extract the bad type for (error)\n> - * reporting. The full header will be extracted to \"hdrbuf\" for use\n> - * with parse_loose_header(), ULHR_TOO_LONG will still be returned\n> - * from this function to indicate that the header was too long.\n> - */\n> -enum unpack_loose_header_result {\n> -\tULHR_OK,\n> -\tULHR_BAD,\n> -\tULHR_TOO_LONG,\n> -};\n> -enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,\n> -\t\t\t\t\t\t    unsigned char *map,\n> -\t\t\t\t\t\t    unsigned long mapsize,\n> -\t\t\t\t\t\t    void *buffer,\n> -\t\t\t\t\t\t    unsigned long bufsiz,\n> -\t\t\t\t\t\t    struct strbuf *hdrbuf);\n> -\n> -/**\n> - * parse_loose_header() parses the starting \"<type> <len>\\0\" of an\n> - * object. If it doesn't follow that format -1 is returned. To check\n> - * the validity of the <type> populate the \"typep\" in the \"struct\n> - * object_info\". It will be OBJ_BAD if the object type is unknown. The\n> - * parsed <len> can be retrieved via \"oi->sizep\", and from there\n> - * passed to unpack_loose_rest().\n> - */\n> -struct object_info;\n> -int parse_loose_header(const char *hdr, struct object_info *oi);\n> -\n> -/**\n> - * With in-core object data in \"buf\", rehash it to make sure the\n> - * object name actually matches \"oid\" to detect object corruption.\n> - *\n> - * A negative value indicates an error, usually that the OID is not\n> - * what we expected, but it might also indicate another error.\n> - */\n> -int check_object_signature(struct repository *r, const struct object_id *oid,\n> -\t\t\t   void *map, unsigned long size,\n> -\t\t\t   enum object_type type);\n> -\n> -/**\n> - * A streaming version of check_object_signature().\n> - * Try reading the object named with \"oid\" using\n> - * the streaming interface and rehash it to do the same.\n> - */\n> -int stream_object_signature(struct repository *r, const struct object_id *oid);\n> -\n> -int finalize_object_file(const char *tmpfile, const char *filename);\n> -\n> -/* Helper to check and \"touch\" a file */\n> -int check_and_freshen_file(const char *fn, int freshen);\n> -\n> -extern const signed char hexval_table[256];\n> -static inline unsigned int hexval(unsigned char c)\n> -{\n> -\treturn hexval_table[c];\n> -}\n> -\n> -/*\n> - * Convert two consecutive hexadecimal digits into a char.  Return a\n> - * negative value on error.  Don't run over the end of short strings.\n> - */\n> -static inline int hex2chr(const char *s)\n> -{\n> -\tunsigned int val = hexval(s[0]);\n> -\treturn (val & ~0xf) ? val : (val << 4) | hexval(s[1]);\n> -}\n> -\n> -/* Convert to/from hex/sha1 representation */\n> -#define MINIMUM_ABBREV minimum_abbrev\n> -#define DEFAULT_ABBREV default_abbrev\n> -\n> -/* used when the code does not know or care what the default abbrev is */\n> -#define FALLBACK_DEFAULT_ABBREV 7\n> -\n> -struct object_context {\n> -\tunsigned short mode;\n> -\t/*\n> -\t * symlink_path is only used by get_tree_entry_follow_symlinks,\n> -\t * and only for symlinks that point outside the repository.\n> -\t */\n> -\tstruct strbuf symlink_path;\n> -\t/*\n> -\t * If GET_OID_RECORD_PATH is set, this will record path (if any)\n> -\t * found when resolving the name. The caller is responsible for\n> -\t * releasing the memory.\n> -\t */\n> -\tchar *path;\n> -};\n> -\n> -#define GET_OID_QUIETLY           01\n> -#define GET_OID_COMMIT            02\n> -#define GET_OID_COMMITTISH        04\n> -#define GET_OID_TREE             010\n> -#define GET_OID_TREEISH          020\n> -#define GET_OID_BLOB             040\n> -#define GET_OID_FOLLOW_SYMLINKS 0100\n> -#define GET_OID_RECORD_PATH     0200\n> -#define GET_OID_ONLY_TO_DIE    04000\n> -#define GET_OID_REQUIRE_PATH  010000\n> -\n> -#define GET_OID_DISAMBIGUATORS \\\n> -\t(GET_OID_COMMIT | GET_OID_COMMITTISH | \\\n> -\tGET_OID_TREE | GET_OID_TREEISH | \\\n> -\tGET_OID_BLOB)\n> -\n> -enum get_oid_result {\n> -\tFOUND = 0,\n> -\tMISSING_OBJECT = -1, /* The requested object is missing */\n> -\tSHORT_NAME_AMBIGUOUS = -2,\n> -\t/* The following only apply when symlinks are followed */\n> -\tDANGLING_SYMLINK = -4, /*\n> -\t\t\t\t* The initial symlink is there, but\n> -\t\t\t\t* (transitively) points to a missing\n> -\t\t\t\t* in-tree file\n> -\t\t\t\t*/\n> -\tSYMLINK_LOOP = -5,\n> -\tNOT_DIR = -6, /*\n> -\t\t       * Somewhere along the symlink chain, a path is\n> -\t\t       * requested which contains a file as a\n> -\t\t       * non-final element.\n> -\t\t       */\n> -};\n> -\n> -int repo_get_oid(struct repository *r, const char *str, struct object_id *oid);\n> -__attribute__((format (printf, 2, 3)))\n> -int get_oidf(struct object_id *oid, const char *fmt, ...);\n> -int repo_get_oid_commit(struct repository *r, const char *str, struct object_id *oid);\n> -int repo_get_oid_committish(struct repository *r, const char *str, struct object_id *oid);\n> -int repo_get_oid_tree(struct repository *r, const char *str, struct object_id *oid);\n> -int repo_get_oid_treeish(struct repository *r, const char *str, struct object_id *oid);\n> -int repo_get_oid_blob(struct repository *r, const char *str, struct object_id *oid);\n> -int repo_get_oid_mb(struct repository *r, const char *str, struct object_id *oid);\n> -void maybe_die_on_misspelt_object_name(struct repository *repo,\n> -\t\t\t\t       const char *name,\n> -\t\t\t\t       const char *prefix);\n> -enum get_oid_result get_oid_with_context(struct repository *repo, const char *str,\n> -\t\t\t\t\t unsigned flags, struct object_id *oid,\n> -\t\t\t\t\t struct object_context *oc);\n> -\n> -#define get_oid(str, oid)\t\trepo_get_oid(the_repository, str, oid)\n> -#define get_oid_commit(str, oid)\trepo_get_oid_commit(the_repository, str, oid)\n> -#define get_oid_committish(str, oid)\trepo_get_oid_committish(the_repository, str, oid)\n> -#define get_oid_tree(str, oid)\t\trepo_get_oid_tree(the_repository, str, oid)\n> -#define get_oid_treeish(str, oid)\trepo_get_oid_treeish(the_repository, str, oid)\n> -#define get_oid_blob(str, oid)\t\trepo_get_oid_blob(the_repository, str, oid)\n> -#define get_oid_mb(str, oid) \t\trepo_get_oid_mb(the_repository, str, oid)\n> -\n> -typedef int each_abbrev_fn(const struct object_id *oid, void *);\n> -int repo_for_each_abbrev(struct repository *r, const char *prefix, each_abbrev_fn, void *);\n> -#define for_each_abbrev(prefix, fn, data) repo_for_each_abbrev(the_repository, prefix, fn, data)\n> -\n> -int set_disambiguate_hint_config(const char *var, const char *value);\n> -\n> -/*\n> - * Try to read a SHA1 in hexadecimal format from the 40 characters\n> - * starting at hex.  Write the 20-byte result to sha1 in binary form.\n> - * Return 0 on success.  Reading stops if a NUL is encountered in the\n> - * input, so it is safe to pass this function an arbitrary\n> - * null-terminated string.\n> - */\n> -int get_sha1_hex(const char *hex, unsigned char *sha1);\n> -int get_oid_hex(const char *hex, struct object_id *sha1);\n> -\n> -/* Like get_oid_hex, but for an arbitrary hash algorithm. */\n> -int get_oid_hex_algop(const char *hex, struct object_id *oid, const struct git_hash_algo *algop);\n> -\n> -/*\n> - * Read `len` pairs of hexadecimal digits from `hex` and write the\n> - * values to `binary` as `len` bytes. Return 0 on success, or -1 if\n> - * the input does not consist of hex digits).\n> - */\n> -int hex_to_bytes(unsigned char *binary, const char *hex, size_t len);\n> -\n> -/*\n> - * Convert a binary hash in \"unsigned char []\" or an object name in\n> - * \"struct object_id *\" to its hex equivalent. The `_r` variant is reentrant,\n> - * and writes the NUL-terminated output to the buffer `out`, which must be at\n> - * least `GIT_MAX_HEXSZ + 1` bytes, and returns a pointer to out for\n> - * convenience.\n> - *\n> - * The non-`_r` variant returns a static buffer, but uses a ring of 4\n> - * buffers, making it safe to make multiple calls for a single statement, like:\n> - *\n> - *   printf(\"%s -> %s\", hash_to_hex(one), hash_to_hex(two));\n> - *   printf(\"%s -> %s\", oid_to_hex(one), oid_to_hex(two));\n> - */\n> -char *hash_to_hex_algop_r(char *buffer, const unsigned char *hash, const struct git_hash_algo *);\n> -char *oid_to_hex_r(char *out, const struct object_id *oid);\n> -char *hash_to_hex_algop(const unsigned char *hash, const struct git_hash_algo *);\t/* static buffer result! */\n> -char *hash_to_hex(const unsigned char *hash);\t\t\t\t\t\t/* same static buffer */\n> -char *oid_to_hex(const struct object_id *oid);\t\t\t\t\t\t/* same static buffer */\n> -\n> -/*\n> - * Parse a 40-character hexadecimal object ID starting from hex, updating the\n> - * pointer specified by end when parsing stops.  The resulting object ID is\n> - * stored in oid.  Returns 0 on success.  Parsing will stop on the first NUL or\n> - * other invalid character.  end is only updated on success; otherwise, it is\n> - * unmodified.\n> - */\n> -int parse_oid_hex(const char *hex, struct object_id *oid, const char **end);\n> -\n> -/* Like parse_oid_hex, but for an arbitrary hash algorithm. */\n> -int parse_oid_hex_algop(const char *hex, struct object_id *oid, const char **end,\n> -\t\t\tconst struct git_hash_algo *algo);\n> -\n> -\n> -/*\n> - * These functions work like get_oid_hex and parse_oid_hex, but they will parse\n> - * a hex value for any algorithm. The algorithm is detected based on the length\n> - * and the algorithm in use is returned. If this is not a hex object ID in any\n> - * algorithm, returns GIT_HASH_UNKNOWN.\n> - */\n> -int get_oid_hex_any(const char *hex, struct object_id *oid);\n> -int parse_oid_hex_any(const char *hex, struct object_id *oid, const char **end);\n> -\n> -/*\n> - * This reads short-hand syntax that not only evaluates to a commit\n> - * object name, but also can act as if the end user spelled the name\n> - * of the branch from the command line.\n> - *\n> - * - \"@{-N}\" finds the name of the Nth previous branch we were on, and\n> - *   places the name of the branch in the given buf and returns the\n> - *   number of characters parsed if successful.\n> - *\n> - * - \"<branch>@{upstream}\" finds the name of the other ref that\n> - *   <branch> is configured to merge with (missing <branch> defaults\n> - *   to the current branch), and places the name of the branch in the\n> - *   given buf and returns the number of characters parsed if\n> - *   successful.\n> - *\n> - * If the input is not of the accepted format, it returns a negative\n> - * number to signal an error.\n> - *\n> - * If the input was ok but there are not N branch switches in the\n> - * reflog, it returns 0.\n> - */\n> -#define INTERPRET_BRANCH_LOCAL (1<<0)\n> -#define INTERPRET_BRANCH_REMOTE (1<<1)\n> -#define INTERPRET_BRANCH_HEAD (1<<2)\n> -struct interpret_branch_name_options {\n> -\t/*\n> -\t * If \"allowed\" is non-zero, it is a treated as a bitfield of allowable\n> -\t * expansions: local branches (\"refs/heads/\"), remote branches\n> -\t * (\"refs/remotes/\"), or \"HEAD\". If no \"allowed\" bits are set, any expansion is\n> -\t * allowed, even ones to refs outside of those namespaces.\n> -\t */\n> -\tunsigned allowed;\n> -\n> -\t/*\n> -\t * If ^{upstream} or ^{push} (or equivalent) is requested, and the\n> -\t * branch in question does not have such a reference, return -1 instead\n> -\t * of die()-ing.\n> -\t */\n> -\tunsigned nonfatal_dangling_mark : 1;\n> -};\n> -int repo_interpret_branch_name(struct repository *r,\n> -\t\t\t       const char *str, int len,\n> -\t\t\t       struct strbuf *buf,\n> -\t\t\t       const struct interpret_branch_name_options *options);\n> -#define interpret_branch_name(str, len, buf, options) \\\n> -\trepo_interpret_branch_name(the_repository, str, len, buf, options)\n> -\n> -int validate_headref(const char *ref);\n> -\n> -int base_name_compare(const char *name1, size_t len1, int mode1,\n> -\t\t      const char *name2, size_t len2, int mode2);\n> -int df_name_compare(const char *name1, size_t len1, int mode1,\n> -\t\t    const char *name2, size_t len2, int mode2);\n> -int name_compare(const char *name1, size_t len1, const char *name2, size_t len2);\n> -int cache_name_stage_compare(const char *name1, int len1, int stage1, const char *name2, int len2, int stage2);\n> -\n> -void *read_object_with_reference(struct repository *r,\n> -\t\t\t\t const struct object_id *oid,\n> -\t\t\t\t enum object_type required_type,\n> -\t\t\t\t unsigned long *size,\n> -\t\t\t\t struct object_id *oid_ret);\n> -\n> -struct object *repo_peel_to_type(struct repository *r,\n> -\t\t\t\t const char *name, int namelen,\n> -\t\t\t\t struct object *o, enum object_type);\n> -#define peel_to_type(name, namelen, obj, type) \\\n> -\trepo_peel_to_type(the_repository, name, namelen, obj, type)\n> -\n> -#define IDENT_STRICT\t       1\n> -#define IDENT_NO_DATE\t       2\n> -#define IDENT_NO_NAME\t       4\n> -\n> -enum want_ident {\n> -\tWANT_BLANK_IDENT,\n> -\tWANT_AUTHOR_IDENT,\n> -\tWANT_COMMITTER_IDENT\n> -};\n> -\n> -const char *git_author_info(int);\n> -const char *git_committer_info(int);\n> -const char *fmt_ident(const char *name, const char *email,\n> -\t\t      enum want_ident whose_ident,\n> -\t\t      const char *date_str, int);\n> -const char *fmt_name(enum want_ident);\n> -const char *ident_default_name(void);\n> -const char *ident_default_email(void);\n> -const char *git_editor(void);\n> -const char *git_sequence_editor(void);\n> -const char *git_pager(int stdout_is_tty);\n> -int is_terminal_dumb(void);\n> -int git_ident_config(const char *, const char *, void *);\n> -/*\n> - * Prepare an ident to fall back on if the user didn't configure it.\n> - */\n> -void prepare_fallback_ident(const char *name, const char *email);\n> -void reset_ident_date(void);\n> -\n> -struct ident_split {\n> -\tconst char *name_begin;\n> -\tconst char *name_end;\n> -\tconst char *mail_begin;\n> -\tconst char *mail_end;\n> -\tconst char *date_begin;\n> -\tconst char *date_end;\n> -\tconst char *tz_begin;\n> -\tconst char *tz_end;\n> -};\n> -/*\n> - * Signals an success with 0, but time part of the result may be NULL\n> - * if the input lacks timestamp and zone\n> - */\n> -int split_ident_line(struct ident_split *, const char *, int);\n> -\n> -/*\n> - * Given a commit or tag object buffer and the commit or tag headers, replaces\n> - * the idents in the headers with their canonical versions using the mailmap mechanism.\n> - */\n> -void apply_mailmap_to_header(struct strbuf *, const char **, struct string_list *);\n> -\n> -/*\n> - * Compare split idents for equality or strict ordering. Note that we\n> - * compare only the ident part of the line, ignoring any timestamp.\n> - *\n> - * Because there are two fields, we must choose one as the primary key; we\n> - * currently arbitrarily pick the email.\n> - */\n> -int ident_cmp(const struct ident_split *, const struct ident_split *);\n> -\n> -struct cache_def {\n> -\tstruct strbuf path;\n> -\tint flags;\n> -\tint track_flags;\n> -\tint prefix_len_stat_func;\n> -};\n> -#define CACHE_DEF_INIT { \\\n> -\t.path = STRBUF_INIT, \\\n> -}\n> -static inline void cache_def_clear(struct cache_def *cache)\n> -{\n> -\tstrbuf_release(&cache->path);\n> -}\n> -\n> -int has_symlink_leading_path(const char *name, int len);\n> -int threaded_has_symlink_leading_path(struct cache_def *, const char *, int);\n> -int check_leading_path(const char *name, int len, int warn_on_lstat_err);\n> -int has_dirs_only_path(const char *name, int len, int prefix_len);\n> -void invalidate_lstat_cache(void);\n> -void schedule_dir_for_removal(const char *name, int len);\n> -void remove_scheduled_dirs(void);\n> -\n> -struct pack_window {\n> -\tstruct pack_window *next;\n> -\tunsigned char *base;\n> -\toff_t offset;\n> -\tsize_t len;\n> -\tunsigned int last_used;\n> -\tunsigned int inuse_cnt;\n> -};\n> -\n> -struct pack_entry {\n> -\toff_t offset;\n> -\tstruct packed_git *p;\n> -};\n> -\n> -/*\n> - * Create a temporary file rooted in the object database directory, or\n> - * die on failure. The filename is taken from \"pattern\", which should have the\n> - * usual \"XXXXXX\" trailer, and the resulting filename is written into the\n> - * \"template\" buffer. Returns the open descriptor.\n> - */\n> -int odb_mkstemp(struct strbuf *temp_filename, const char *pattern);\n> -\n> -/*\n> - * Create a pack .keep file named \"name\" (which should generally be the output\n> - * of odb_pack_name). Returns a file descriptor opened for writing, or -1 on\n> - * error.\n> - */\n> -int odb_pack_keep(const char *name);\n> -\n> -/*\n> - * Set this to 0 to prevent oid_object_info_extended() from fetching missing\n> - * blobs. This has a difference only if extensions.partialClone is set.\n> - *\n> - * Its default value is 1.\n> - */\n> -extern int fetch_if_missing;\n> -\n> -/* Dumb servers support */\n> -int update_server_info(int);\n> -\n> -const char *get_log_output_encoding(void);\n> -const char *get_commit_output_encoding(void);\n> -\n> -int committer_ident_sufficiently_given(void);\n> -int author_ident_sufficiently_given(void);\n> -\n> -extern const char *git_commit_encoding;\n> -extern const char *git_log_output_encoding;\n> -extern const char *git_mailmap_file;\n> -extern const char *git_mailmap_blob;\n> -\n> -/* IO helper functions */\n> -void maybe_flush_or_die(FILE *, const char *);\n> -__attribute__((format (printf, 2, 3)))\n> -void fprintf_or_die(FILE *, const char *fmt, ...);\n> -void fwrite_or_die(FILE *f, const void *buf, size_t count);\n> -void fflush_or_die(FILE *f);\n> -\n> -#define COPY_READ_ERROR (-2)\n> -#define COPY_WRITE_ERROR (-3)\n> -int copy_fd(int ifd, int ofd);\n> -int copy_file(const char *dst, const char *src, int mode);\n> -int copy_file_with_time(const char *dst, const char *src, int mode);\n> -\n> -/*\n> - * Compare the file mode and contents of two given files.\n> - *\n> - * If both files are actually symbolic links, the function returns 1 if the link\n> - * targets are identical or 0 if they are not.\n> - *\n> - * If any of the two files cannot be accessed or in case of read failures, this\n> - * function returns 0.\n> - *\n> - * If the file modes and contents are identical, the function returns 1,\n> - * otherwise it returns 0.\n> - */\n> -int do_files_match(const char *path1, const char *path2);\n> -\n> -void write_or_die(int fd, const void *buf, size_t count);\n> -void fsync_or_die(int fd, const char *);\n> -int fsync_component(enum fsync_component component, int fd);\n> -void fsync_component_or_die(enum fsync_component component, int fd, const char *msg);\n> -\n> -static inline int batch_fsync_enabled(enum fsync_component component)\n> -{\n> -\treturn (fsync_components & component) && (fsync_method == FSYNC_METHOD_BATCH);\n> -}\n> -\n> -ssize_t read_in_full(int fd, void *buf, size_t count);\n> -ssize_t write_in_full(int fd, const void *buf, size_t count);\n> -ssize_t pread_in_full(int fd, void *buf, size_t count, off_t offset);\n> -\n> -static inline ssize_t write_str_in_full(int fd, const char *str)\n> -{\n> -\treturn write_in_full(fd, str, strlen(str));\n> -}\n> -\n> -/**\n> - * Open (and truncate) the file at path, write the contents of buf to it,\n> - * and close it. Dies if any errors are encountered.\n> - */\n> -void write_file_buf(const char *path, const char *buf, size_t len);\n> -\n> -/**\n> - * Like write_file_buf(), but format the contents into a buffer first.\n> - * Additionally, write_file() will append a newline if one is not already\n> - * present, making it convenient to write text files:\n> - *\n> - *   write_file(path, \"counter: %d\", ctr);\n> - */\n> -__attribute__((format (printf, 2, 3)))\n> -void write_file(const char *path, const char *fmt, ...);\n> -\n> -/* pager.c */\n> -void setup_pager(void);\n> -int pager_in_use(void);\n> -extern int pager_use_color;\n> -int term_columns(void);\n> -void term_clear_line(void);\n> -int decimal_width(uintmax_t);\n> -int check_pager_config(const char *cmd);\n> -void prepare_pager_args(struct child_process *, const char *pager);\n> -\n> -extern const char *editor_program;\n> -extern const char *askpass_program;\n> -extern const char *excludes_file;\n> -\n> -/* base85 */\n> -int decode_85(char *dst, const char *line, int linelen);\n> -void encode_85(char *buf, const unsigned char *data, int bytes);\n> -\n> -/* pkt-line.c */\n> -void packet_trace_identity(const char *prog);\n> -=======\n> -/*\n> - * Return an abbreviated sha1 unique within this repository's object database.\n> - * The result will be at least `len` characters long, and will be NUL\n> - * terminated.\n> - *\n> - * The non-`_r` version returns a static buffer which remains valid until 4\n> - * more calls to find_unique_abbrev are made.\n> - *\n> - * The `_r` variant writes to a buffer supplied by the caller, which must be at\n> - * least `GIT_MAX_HEXSZ + 1` bytes. The return value is the number of bytes\n> - * written (excluding the NUL terminator).\n> - *\n> - * Note that while this version avoids the static buffer, it is not fully\n> - * reentrant, as it calls into other non-reentrant git code.\n> - */\n> -const char *repo_find_unique_abbrev(struct repository *r, const struct object_id *oid, int len);\n> -#define find_unique_abbrev(oid, len) repo_find_unique_abbrev(the_repository, oid, len)\n> -int repo_find_unique_abbrev_r(struct repository *r, char *hex, const struct object_id *oid, int len);\n> -#define find_unique_abbrev_r(hex, oid, len) repo_find_unique_abbrev_r(the_repository, hex, oid, len)\n> -\n> -/* set default permissions by passing mode arguments to open(2) */\n> -int git_mkstemps_mode(char *pattern, int suffix_len, int mode);\n> -int git_mkstemp_mode(char *pattern, int mode);\n> -\n> -/*\n> - * NOTE NOTE NOTE!!\n> - *\n> - * PERM_UMASK, OLD_PERM_GROUP and OLD_PERM_EVERYBODY enumerations must\n> - * not be changed. Old repositories have core.sharedrepository written in\n> - * numeric format, and therefore these values are preserved for compatibility\n> - * reasons.\n> - */\n> -enum sharedrepo {\n> -\tPERM_UMASK          = 0,\n> -\tOLD_PERM_GROUP      = 1,\n> -\tOLD_PERM_EVERYBODY  = 2,\n> -\tPERM_GROUP          = 0660,\n> -\tPERM_EVERYBODY      = 0664\n> -};\n> -int git_config_perm(const char *var, const char *value);\n> -int adjust_shared_perm(const char *path);\n> -\n> -/*\n> - * Create the directory containing the named path, using care to be\n> - * somewhat safe against races. Return one of the scld_error values to\n> - * indicate success/failure. On error, set errno to describe the\n> - * problem.\n> - *\n> - * SCLD_VANISHED indicates that one of the ancestor directories of the\n> - * path existed at one point during the function call and then\n> - * suddenly vanished, probably because another process pruned the\n> - * directory while we were working.  To be robust against this kind of\n> - * race, callers might want to try invoking the function again when it\n> - * returns SCLD_VANISHED.\n> - *\n> - * safe_create_leading_directories() temporarily changes path while it\n> - * is working but restores it before returning.\n> - * safe_create_leading_directories_const() doesn't modify path, even\n> - * temporarily. Both these variants adjust the permissions of the\n> - * created directories to honor core.sharedRepository, so they are best\n> - * suited for files inside the git dir. For working tree files, use\n> - * safe_create_leading_directories_no_share() instead, as it ignores\n> - * the core.sharedRepository setting.\n> - */\n> -enum scld_error {\n> -\tSCLD_OK = 0,\n> -\tSCLD_FAILED = -1,\n> -\tSCLD_PERMS = -2,\n> -\tSCLD_EXISTS = -3,\n> -\tSCLD_VANISHED = -4\n> -};\n> -enum scld_error safe_create_leading_directories(char *path);\n> -enum scld_error safe_create_leading_directories_const(const char *path);\n> -enum scld_error safe_create_leading_directories_no_share(char *path);\n> -\n> -int mkdir_in_gitdir(const char *path);\n> -char *interpolate_path(const char *path, int real_home);\n> -/* NEEDSWORK: remove this synonym once in-flight topics have migrated */\n> -#define expand_user_path interpolate_path\n> -const char *enter_repo(const char *path, int strict);\n> -static inline int is_absolute_path(const char *path)\n> -{\n> -\treturn is_dir_sep(path[0]) || has_dos_drive_prefix(path);\n> -}\n> -int is_directory(const char *);\n> -char *strbuf_realpath(struct strbuf *resolved, const char *path,\n> -\t\t      int die_on_error);\n> -char *strbuf_realpath_forgiving(struct strbuf *resolved, const char *path,\n> -\t\t\t\tint die_on_error);\n> -char *real_pathdup(const char *path, int die_on_error);\n> -const char *absolute_path(const char *path);\n> -char *absolute_pathdup(const char *path);\n> -const char *remove_leading_path(const char *in, const char *prefix);\n> -const char *relative_path(const char *in, const char *prefix, struct strbuf *sb);\n> -int normalize_path_copy_len(char *dst, const char *src, int *prefix_len);\n> -int normalize_path_copy(char *dst, const char *src);\n> -int longest_ancestor_length(const char *path, struct string_list *prefixes);\n> -char *strip_path_suffix(const char *path, const char *suffix);\n> -int daemon_avoid_alias(const char *path);\n> -\n> -/*\n> - * These functions match their is_hfs_dotgit() counterparts; see utf8.h for\n> - * details.\n> - */\n> -int is_ntfs_dotgit(const char *name);\n> -int is_ntfs_dotgitmodules(const char *name);\n> -int is_ntfs_dotgitignore(const char *name);\n> -int is_ntfs_dotgitattributes(const char *name);\n> -int is_ntfs_dotmailmap(const char *name);\n> -\n> -/*\n> - * Returns true iff \"str\" could be confused as a command-line option when\n> - * passed to a sub-program like \"ssh\". Note that this has nothing to do with\n> - * shell-quoting, which should be handled separately; we're assuming here that\n> - * the string makes it verbatim to the sub-program.\n> - */\n> -int looks_like_command_line_option(const char *str);\n> -\n> -/**\n> - * Return a newly allocated string with the evaluation of\n> - * \"$XDG_CONFIG_HOME/$subdir/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n> - * \"$HOME/.config/$subdir/$filename\". Return NULL upon error.\n> - */\n> -char *xdg_config_home_for(const char *subdir, const char *filename);\n> -\n> -/**\n> - * Return a newly allocated string with the evaluation of\n> - * \"$XDG_CONFIG_HOME/git/$filename\" if $XDG_CONFIG_HOME is non-empty, otherwise\n> - * \"$HOME/.config/git/$filename\". Return NULL upon error.\n> - */\n> -char *xdg_config_home(const char *filename);\n> -\n> -/**\n> - * Return a newly allocated string with the evaluation of\n> - * \"$XDG_CACHE_HOME/git/$filename\" if $XDG_CACHE_HOME is non-empty, otherwise\n> - * \"$HOME/.cache/git/$filename\". Return NULL upon error.\n> - */\n> -char *xdg_cache_home(const char *filename);\n> -\n> -int git_open_cloexec(const char *name, int flags);\n> -#define git_open(name) git_open_cloexec(name, O_RDONLY)\n> -\n> -/**\n> - * unpack_loose_header() initializes the data stream needed to unpack\n> - * a loose object header.\n> - *\n> - * Returns:\n> - *\n> - * - ULHR_OK on success\n> - * - ULHR_BAD on error\n> - * - ULHR_TOO_LONG if the header was too long\n> - *\n> - * It will only parse up to MAX_HEADER_LEN bytes unless an optional\n> - * \"hdrbuf\" argument is non-NULL. This is intended for use with\n> - * OBJECT_INFO_ALLOW_UNKNOWN_TYPE to extract the bad type for (error)\n> - * reporting. The full header will be extracted to \"hdrbuf\" for use\n> - * with parse_loose_header(), ULHR_TOO_LONG will still be returned\n> - * from this function to indicate that the header was too long.\n> - */\n> -enum unpack_loose_header_result {\n> -\tULHR_OK,\n> -\tULHR_BAD,\n> -\tULHR_TOO_LONG,\n> -};\n> -enum unpack_loose_header_result unpack_loose_header(git_zstream *stream,\n> -\t\t\t\t\t\t    unsigned char *map,\n> -\t\t\t\t\t\t    unsigned long mapsize,\n> -\t\t\t\t\t\t    void *buffer,\n> -\t\t\t\t\t\t    unsigned long bufsiz,\n> -\t\t\t\t\t\t    struct strbuf *hdrbuf);\n> -\n> -/**\n> - * parse_loose_header() parses the starting \"<type> <len>\\0\" of an\n> - * object. If it doesn't follow that format -1 is returned. To check\n> - * the validity of the <type> populate the \"typep\" in the \"struct\n> - * object_info\". It will be OBJ_BAD if the object type is unknown. The\n> - * parsed <len> can be retrieved via \"oi->sizep\", and from there\n> - * passed to unpack_loose_rest().\n> - */\n> -struct object_info;\n> -int parse_loose_header(const char *hdr, struct object_info *oi);\n> -\n> -/**\n> - * With in-core object data in \"buf\", rehash it to make sure the\n> - * object name actually matches \"oid\" to detect object corruption.\n> - *\n> - * A negative value indicates an error, usually that the OID is not\n> - * what we expected, but it might also indicate another error.\n> - */\n> -int check_object_signature(struct repository *r, const struct object_id *oid,\n> -\t\t\t   void *map, unsigned long size,\n> -\t\t\t   enum object_type type);\n> -\n> -/**\n> - * A streaming version of check_object_signature().\n> - * Try reading the object named with \"oid\" using\n> - * the streaming interface and rehash it to do the same.\n> - */\n> -int stream_object_signature(struct repository *r, const struct object_id *oid);\n> -\n> -int finalize_object_file(const char *tmpfile, const char *filename);\n> -\n> -/* Helper to check and \"touch\" a file */\n> -int check_and_freshen_file(const char *fn, int freshen);\n> -\n> -extern const signed char hexval_table[256];\n> -static inline unsigned int hexval(unsigned char c)\n> -{\n> -\treturn hexval_table[c];\n> -}\n> -\n> -/*\n> - * Convert two consecutive hexadecimal digits into a char.  Return a\n> - * negative value on error.  Don't run over the end of short strings.\n> - */\n> -static inline int hex2chr(const char *s)\n> -{\n> -\tunsigned int val = hexval(s[0]);\n> -\treturn (val & ~0xf) ? val : (val << 4) | hexval(s[1]);\n> -}\n> -\n> -/* Convert to/from hex/sha1 representation */\n> -#define MINIMUM_ABBREV minimum_abbrev\n> -#define DEFAULT_ABBREV default_abbrev\n> -\n> -/* used when the code does not know or care what the default abbrev is */\n> -#define FALLBACK_DEFAULT_ABBREV 7\n> -\n> -struct object_context {\n> -\tunsigned short mode;\n> -\t/*\n> -\t * symlink_path is only used by get_tree_entry_follow_symlinks,\n> -\t * and only for symlinks that point outside the repository.\n> -\t */\n> -\tstruct strbuf symlink_path;\n> -\t/*\n> -\t * If GET_OID_RECORD_PATH is set, this will record path (if any)\n> -\t * found when resolving the name. The caller is responsible for\n> -\t * releasing the memory.\n> -\t */\n> -\tchar *path;\n> -};\n> -\n> -#define GET_OID_QUIETLY           01\n> -#define GET_OID_COMMIT            02\n> -#define GET_OID_COMMITTISH        04\n> -#define GET_OID_TREE             010\n> -#define GET_OID_TREEISH          020\n> -#define GET_OID_BLOB             040\n> -#define GET_OID_FOLLOW_SYMLINKS 0100\n> -#define GET_OID_RECORD_PATH     0200\n> -#define GET_OID_ONLY_TO_DIE    04000\n> -#define GET_OID_REQUIRE_PATH  010000\n> -\n> -#define GET_OID_DISAMBIGUATORS \\\n> -\t(GET_OID_COMMIT | GET_OID_COMMITTISH | \\\n> -\tGET_OID_TREE | GET_OID_TREEISH | \\\n> -\tGET_OID_BLOB)\n> -\n> -enum get_oid_result {\n> -\tFOUND = 0,\n> -\tMISSING_OBJECT = -1, /* The requested object is missing */\n> -\tSHORT_NAME_AMBIGUOUS = -2,\n> -\t/* The following only apply when symlinks are followed */\n> -\tDANGLING_SYMLINK = -4, /*\n> -\t\t\t\t* The initial symlink is there, but\n> -\t\t\t\t* (transitively) points to a missing\n> -\t\t\t\t* in-tree file\n> -\t\t\t\t*/\n> -\tSYMLINK_LOOP = -5,\n> -\tNOT_DIR = -6, /*\n> -\t\t       * Somewhere along the symlink chain, a path is\n> -\t\t       * requested which contains a file as a\n> -\t\t       * non-final element.\n> -\t\t       */\n> -};\n> -\n> -int repo_get_oid(struct repository *r, const char *str, struct object_id *oid);\n> -__attribute__((format (printf, 2, 3)))\n> -int get_oidf(struct object_id *oid, const char *fmt, ...);\n> -int repo_get_oid_commit(struct repository *r, const char *str, struct object_id *oid);\n> -int repo_get_oid_committish(struct repository *r, const char *str, struct object_id *oid);\n> -int repo_get_oid_tree(struct repository *r, const char *str, struct object_id *oid);\n> -int repo_get_oid_treeish(struct repository *r, const char *str, struct object_id *oid);\n> -int repo_get_oid_blob(struct repository *r, const char *str, struct object_id *oid);\n> -int repo_get_oid_mb(struct repository *r, const char *str, struct object_id *oid);\n> -void maybe_die_on_misspelt_object_name(struct repository *repo,\n> -\t\t\t\t       const char *name,\n> -\t\t\t\t       const char *prefix);\n> -enum get_oid_result get_oid_with_context(struct repository *repo, const char *str,\n> -\t\t\t\t\t unsigned flags, struct object_id *oid,\n> -\t\t\t\t\t struct object_context *oc);\n> -\n> -#define get_oid(str, oid)\t\trepo_get_oid(the_repository, str, oid)\n> -#define get_oid_commit(str, oid)\trepo_get_oid_commit(the_repository, str, oid)\n> -#define get_oid_committish(str, oid)\trepo_get_oid_committish(the_repository, str, oid)\n> -#define get_oid_tree(str, oid)\t\trepo_get_oid_tree(the_repository, str, oid)\n> -#define get_oid_treeish(str, oid)\trepo_get_oid_treeish(the_repository, str, oid)\n> -#define get_oid_blob(str, oid)\t\trepo_get_oid_blob(the_repository, str, oid)\n> -#define get_oid_mb(str, oid) \t\trepo_get_oid_mb(the_repository, str, oid)\n> -\n> -typedef int each_abbrev_fn(const struct object_id *oid, void *);\n> -int repo_for_each_abbrev(struct repository *r, const char *prefix, each_abbrev_fn, void *);\n> -#define for_each_abbrev(prefix, fn, data) repo_for_each_abbrev(the_repository, prefix, fn, data)\n> -\n> -int set_disambiguate_hint_config(const char *var, const char *value);\n> -\n> -/*\n> - * Try to read a SHA1 in hexadecimal format from the 40 characters\n> - * starting at hex.  Write the 20-byte result to sha1 in binary form.\n> - * Return 0 on success.  Reading stops if a NUL is encountered in the\n> - * input, so it is safe to pass this function an arbitrary\n> - * null-terminated string.\n> - */\n> -int get_sha1_hex(const char *hex, unsigned char *sha1);\n> -int get_oid_hex(const char *hex, struct object_id *sha1);\n> -\n> -/* Like get_oid_hex, but for an arbitrary hash algorithm. */\n> -int get_oid_hex_algop(const char *hex, struct object_id *oid, const struct git_hash_algo *algop);\n> -\n> -/*\n> - * Read `len` pairs of hexadecimal digits from `hex` and write the\n> - * values to `binary` as `len` bytes. Return 0 on success, or -1 if\n> - * the input does not consist of hex digits).\n> - */\n> -int hex_to_bytes(unsigned char *binary, const char *hex, size_t len);\n> -\n> -/*\n> - * Convert a binary hash in \"unsigned char []\" or an object name in\n> - * \"struct object_id *\" to its hex equivalent. The `_r` variant is reentrant,\n> - * and writes the NUL-terminated output to the buffer `out`, which must be at\n> - * least `GIT_MAX_HEXSZ + 1` bytes, and returns a pointer to out for\n> - * convenience.\n> - *\n> - * The non-`_r` variant returns a static buffer, but uses a ring of 4\n> - * buffers, making it safe to make multiple calls for a single statement, like:\n> - *\n> - *   printf(\"%s -> %s\", hash_to_hex(one), hash_to_hex(two));\n> - *   printf(\"%s -> %s\", oid_to_hex(one), oid_to_hex(two));\n> - */\n> -char *hash_to_hex_algop_r(char *buffer, const unsigned char *hash, const struct git_hash_algo *);\n> -char *oid_to_hex_r(char *out, const struct object_id *oid);\n> -char *hash_to_hex_algop(const unsigned char *hash, const struct git_hash_algo *);\t/* static buffer result! */\n> -char *hash_to_hex(const unsigned char *hash);\t\t\t\t\t\t/* same static buffer */\n> -char *oid_to_hex(const struct object_id *oid);\t\t\t\t\t\t/* same static buffer */\n> -\n> -/*\n> - * Parse a 40-character hexadecimal object ID starting from hex, updating the\n> - * pointer specified by end when parsing stops.  The resulting object ID is\n> - * stored in oid.  Returns 0 on success.  Parsing will stop on the first NUL or\n> - * other invalid character.  end is only updated on success; otherwise, it is\n> - * unmodified.\n> - */\n> -int parse_oid_hex(const char *hex, struct object_id *oid, const char **end);\n> -\n> -/* Like parse_oid_hex, but for an arbitrary hash algorithm. */\n> -int parse_oid_hex_algop(const char *hex, struct object_id *oid, const char **end,\n> -\t\t\tconst struct git_hash_algo *algo);\n> -\n> -\n> -/*\n> - * These functions work like get_oid_hex and parse_oid_hex, but they will parse\n> - * a hex value for any algorithm. The algorithm is detected based on the length\n> - * and the algorithm in use is returned. If this is not a hex object ID in any\n> - * algorithm, returns GIT_HASH_UNKNOWN.\n> - */\n> -int get_oid_hex_any(const char *hex, struct object_id *oid);\n> -int parse_oid_hex_any(const char *hex, struct object_id *oid, const char **end);\n> -\n> -/*\n> - * This reads short-hand syntax that not only evaluates to a commit\n> - * object name, but also can act as if the end user spelled the name\n> - * of the branch from the command line.\n> - *\n> - * - \"@{-N}\" finds the name of the Nth previous branch we were on, and\n> - *   places the name of the branch in the given buf and returns the\n> - *   number of characters parsed if successful.\n> - *\n> - * - \"<branch>@{upstream}\" finds the name of the other ref that\n> - *   <branch> is configured to merge with (missing <branch> defaults\n> - *   to the current branch), and places the name of the branch in the\n> - *   given buf and returns the number of characters parsed if\n> - *   successful.\n> - *\n> - * If the input is not of the accepted format, it returns a negative\n> - * number to signal an error.\n> - *\n> - * If the input was ok but there are not N branch switches in the\n> - * reflog, it returns 0.\n> - */\n> -#define INTERPRET_BRANCH_LOCAL (1<<0)\n> -#define INTERPRET_BRANCH_REMOTE (1<<1)\n> -#define INTERPRET_BRANCH_HEAD (1<<2)\n> -struct interpret_branch_name_options {\n> -\t/*\n> -\t * If \"allowed\" is non-zero, it is a treated as a bitfield of allowable\n> -\t * expansions: local branches (\"refs/heads/\"), remote branches\n> -\t * (\"refs/remotes/\"), or \"HEAD\". If no \"allowed\" bits are set, any expansion is\n> -\t * allowed, even ones to refs outside of those namespaces.\n> -\t */\n> -\tunsigned allowed;\n> -\n> -\t/*\n> -\t * If ^{upstream} or ^{push} (or equivalent) is requested, and the\n> -\t * branch in question does not have such a reference, return -1 instead\n> -\t * of die()-ing.\n> -\t */\n> -\tunsigned nonfatal_dangling_mark : 1;\n> -};\n> -int repo_interpret_branch_name(struct repository *r,\n> -\t\t\t       const char *str, int len,\n> -\t\t\t       struct strbuf *buf,\n> -\t\t\t       const struct interpret_branch_name_options *options);\n> -#define interpret_branch_name(str, len, buf, options) \\\n> -\trepo_interpret_branch_name(the_repository, str, len, buf, options)\n> -\n> -int validate_headref(const char *ref);\n> -\n> -int base_name_compare(const char *name1, size_t len1, int mode1,\n> -\t\t      const char *name2, size_t len2, int mode2);\n> -int df_name_compare(const char *name1, size_t len1, int mode1,\n> -\t\t    const char *name2, size_t len2, int mode2);\n> -int name_compare(const char *name1, size_t len1, const char *name2, size_t len2);\n> -int cache_name_stage_compare(const char *name1, int len1, int stage1, const char *name2, int len2, int stage2);\n> -\n> -void *read_object_with_reference(struct repository *r,\n> -\t\t\t\t const struct object_id *oid,\n> -\t\t\t\t enum object_type required_type,\n> -\t\t\t\t unsigned long *size,\n> -\t\t\t\t struct object_id *oid_ret);\n> -\n> -struct object *repo_peel_to_type(struct repository *r,\n> -\t\t\t\t const char *name, int namelen,\n> -\t\t\t\t struct object *o, enum object_type);\n> -#define peel_to_type(name, namelen, obj, type) \\\n> -\trepo_peel_to_type(the_repository, name, namelen, obj, type)\n> -\n> -#define IDENT_STRICT\t       1\n> -#define IDENT_NO_DATE\t       2\n> -#define IDENT_NO_NAME\t       4\n> -\n> -enum want_ident {\n> -\tWANT_BLANK_IDENT,\n> -\tWANT_AUTHOR_IDENT,\n> -\tWANT_COMMITTER_IDENT\n> -};\n> -\n> -const char *git_author_info(int);\n> -const char *git_committer_info(int);\n> -const char *fmt_ident(const char *name, const char *email,\n> -\t\t      enum want_ident whose_ident,\n> -\t\t      const char *date_str, int);\n> -const char *fmt_name(enum want_ident);\n> -const char *ident_default_name(void);\n> -const char *ident_default_email(void);\n> -const char *git_editor(void);\n> -const char *git_sequence_editor(void);\n> -const char *git_pager(int stdout_is_tty);\n> -int is_terminal_dumb(void);\n> -int git_ident_config(const char *, const char *, void *);\n> -/*\n> - * Prepare an ident to fall back on if the user didn't configure it.\n> - */\n> -void prepare_fallback_ident(const char *name, const char *email);\n> -void reset_ident_date(void);\n> -\n> -struct ident_split {\n> -\tconst char *name_begin;\n> -\tconst char *name_end;\n> -\tconst char *mail_begin;\n> -\tconst char *mail_end;\n> -\tconst char *date_begin;\n> -\tconst char *date_end;\n> -\tconst char *tz_begin;\n> -\tconst char *tz_end;\n> -};\n> -/*\n> - * Signals an success with 0, but time part of the result may be NULL\n> - * if the input lacks timestamp and zone\n> - */\n> -int split_ident_line(struct ident_split *, const char *, int);\n> -\n> -/*\n> - * Given a commit or tag object buffer and the commit or tag headers, replaces\n> - * the idents in the headers with their canonical versions using the mailmap mechanism.\n> - */\n> -void apply_mailmap_to_header(struct strbuf *, const char **, struct string_list *);\n> -\n> -/*\n> - * Compare split idents for equality or strict ordering. Note that we\n> - * compare only the ident part of the line, ignoring any timestamp.\n> - *\n> - * Because there are two fields, we must choose one as the primary key; we\n> - * currently arbitrarily pick the email.\n> - */\n> -int ident_cmp(const struct ident_split *, const struct ident_split *);\n> -\n> -struct cache_def {\n> -\tstruct strbuf path;\n> -\tint flags;\n> -\tint track_flags;\n> -\tint prefix_len_stat_func;\n> -};\n> -#define CACHE_DEF_INIT { \\\n> -\t.path = STRBUF_INIT, \\\n> -}\n> -static inline void cache_def_clear(struct cache_def *cache)\n> -{\n> -\tstrbuf_release(&cache->path);\n> -}\n> -\n> -int has_symlink_leading_path(const char *name, int len);\n> -int threaded_has_symlink_leading_path(struct cache_def *, const char *, int);\n> -int check_leading_path(const char *name, int len, int warn_on_lstat_err);\n> -int has_dirs_only_path(const char *name, int len, int prefix_len);\n> -void invalidate_lstat_cache(void);\n> -void schedule_dir_for_removal(const char *name, int len);\n> -void remove_scheduled_dirs(void);\n> -\n> -struct pack_window {\n> -\tstruct pack_window *next;\n> -\tunsigned char *base;\n> -\toff_t offset;\n> -\tsize_t len;\n> -\tunsigned int last_used;\n> -\tunsigned int inuse_cnt;\n> -};\n> -\n> -struct pack_entry {\n> -\toff_t offset;\n> -\tstruct packed_git *p;\n> -};\n> -\n> -/*\n> - * Create a temporary file rooted in the object database directory, or\n> - * die on failure. The filename is taken from \"pattern\", which should have the\n> - * usual \"XXXXXX\" trailer, and the resulting filename is written into the\n> - * \"template\" buffer. Returns the open descriptor.\n> - */\n> -int odb_mkstemp(struct strbuf *temp_filename, const char *pattern);\n> -\n> -/*\n> - * Create a pack .keep file named \"name\" (which should generally be the output\n> - * of odb_pack_name). Returns a file descriptor opened for writing, or -1 on\n> - * error.\n> - */\n> -int odb_pack_keep(const char *name);\n> -\n> -/*\n> - * Set this to 0 to prevent oid_object_info_extended() from fetching missing\n> - * blobs. This has a difference only if extensions.partialClone is set.\n> - *\n> - * Its default value is 1.\n> - */\n> -extern int fetch_if_missing;\n> -\n> -/* Dumb servers support */\n> -int update_server_info(int);\n> -\n> -const char *get_log_output_encoding(void);\n> -const char *get_commit_output_encoding(void);\n> -\n> -int committer_ident_sufficiently_given(void);\n> -int author_ident_sufficiently_given(void);\n> -\n> -extern const char *git_commit_encoding;\n> -extern const char *git_log_output_encoding;\n> -extern const char *git_mailmap_file;\n> -extern const char *git_mailmap_blob;\n> -\n> -/* IO helper functions */\n> -void maybe_flush_or_die(FILE *, const char *);\n> -__attribute__((format (printf, 2, 3)))\n> -void fprintf_or_die(FILE *, const char *fmt, ...);\n> -void fwrite_or_die(FILE *f, const void *buf, size_t count);\n> -void fflush_or_die(FILE *f);\n> -\n> -#define COPY_READ_ERROR (-2)\n> -#define COPY_WRITE_ERROR (-3)\n> -int copy_fd(int ifd, int ofd);\n> -int copy_file(const char *dst, const char *src, int mode);\n> -int copy_file_with_time(const char *dst, const char *src, int mode);\n> -\n> -void write_or_die(int fd, const void *buf, size_t count);\n> -void fsync_or_die(int fd, const char *);\n> -int fsync_component(enum fsync_component component, int fd);\n> -void fsync_component_or_die(enum fsync_component component, int fd, const char *msg);\n> -\n> -static inline int batch_fsync_enabled(enum fsync_component component)\n> -{\n> -\treturn (fsync_components & component) && (fsync_method == FSYNC_METHOD_BATCH);\n> -}\n> -\n> -ssize_t read_in_full(int fd, void *buf, size_t count);\n> -ssize_t write_in_full(int fd, const void *buf, size_t count);\n> -ssize_t pread_in_full(int fd, void *buf, size_t count, off_t offset);\n> -\n> -static inline ssize_t write_str_in_full(int fd, const char *str)\n> -{\n> -\treturn write_in_full(fd, str, strlen(str));\n> -}\n> -\n> -/**\n> - * Open (and truncate) the file at path, write the contents of buf to it,\n> - * and close it. Dies if any errors are encountered.\n> - */\n> -void write_file_buf(const char *path, const char *buf, size_t len);\n> -\n> -/**\n> - * Like write_file_buf(), but format the contents into a buffer first.\n> - * Additionally, write_file() will append a newline if one is not already\n> - * present, making it convenient to write text files:\n> - *\n> - *   write_file(path, \"counter: %d\", ctr);\n> - */\n> -__attribute__((format (printf, 2, 3)))\n> -void write_file(const char *path, const char *fmt, ...);\n> -\n> -/* pager.c */\n> -void setup_pager(void);\n> -int pager_in_use(void);\n> -extern int pager_use_color;\n> -int term_columns(void);\n> -void term_clear_line(void);\n> -int decimal_width(uintmax_t);\n> -int check_pager_config(const char *cmd);\n> -void prepare_pager_args(struct child_process *, const char *pager);\n> -\n> -extern const char *editor_program;\n> -extern const char *askpass_program;\n> -extern const char *excludes_file;\n> -\n> -/* base85 */\n> -int decode_85(char *dst, const char *line, int linelen);\n> -void encode_85(char *buf, const unsigned char *data, int bytes);\n> -\n> -/* pkt-line.c */\n> -void packet_trace_identity(const char *prog);\n> ->>>>>>> 752e921355 (Merge branch 'js/fix-clone-w-hooks-2.39' into HEAD)\n>\n>  /* add */\n>  /*\n> diff --git a/hook.c b/hook.c\n> remerge CONFLICT (content): Merge conflict in hook.c\n> index ce71f9d01a..b56f97949b 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -5,15 +5,12 @@\n>  #include \"path.h\"\n>  #include \"run-command.h\"\n>  #include \"config.h\"\n> -<<<<<<< 0f15832059 (Git 2.41.1)\n>  #include \"strbuf.h\"\n>  #include \"environment.h\"\n>  #include \"setup.h\"\n>  #include \"copy.h\"\n> -||||||| b9b439e0e3\n> -=======\n>  #include \"strmap.h\"\n> ->>>>>>> 752e921355 (Merge branch 'js/fix-clone-w-hooks-2.39' into HEAD)\n> +#include \"hex.h\"\n>\n>  static struct strset safe_hook_sha256s = STRSET_INIT;\n>  static int safe_hook_sha256s_initialized;\n> * js/fix-clone-w-hooks-2.42\n> diff --git a/Makefile b/Makefile\n> remerge CONFLICT (content): Merge conflict in Makefile\n> index 5ae5f3c864..5776309365 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -2743,16 +2743,8 @@ exec-cmd.sp exec-cmd.s exec-cmd.o: EXTRA_CPPFLAGS = \\\n>  \t'-DBINDIR=\"$(bindir_relative_SQ)\"' \\\n>  \t'-DFALLBACK_RUNTIME_PREFIX=\"$(prefix_SQ)\"'\n>\n> -<<<<<<< babb4e5d71 (Git 2.42.2)\n>  setup.sp setup.s setup.o: GIT-PREFIX\n>  setup.sp setup.s setup.o: EXTRA_CPPFLAGS = \\\n> -||||||| 0f15832059\n> -builtin/init-db.sp builtin/init-db.s builtin/init-db.o: GIT-PREFIX\n> -builtin/init-db.sp builtin/init-db.s builtin/init-db.o: EXTRA_CPPFLAGS = \\\n> -=======\n> -builtin/init-db.sp builtin/init-db.s builtin/init-db.o: GIT-PREFIX\n> -setup.sp setup.s setup.o: EXTRA_CPPFLAGS = \\\n> ->>>>>>> c7b6b0a28f (Merge branch 'js/fix-clone-w-hooks-2.40' into HEAD)\n>  \t-DDEFAULT_GIT_TEMPLATE_DIR='\"$(template_dir_SQ)\"'\n>\n>  config.sp config.s config.o: GIT-PREFIX\n> diff --git a/builtin/init-db.c b/builtin/init-db.c\n> remerge CONFLICT (content): Merge conflict in builtin/init-db.c\n> index 6df47dd078..cb727c826f 100644\n> --- a/builtin/init-db.c\n> +++ b/builtin/init-db.c\n> @@ -12,920 +12,7 @@\n>  #include \"parse-options.h\"\n>  #include \"path.h\"\n>  #include \"setup.h\"\n> -<<<<<<< babb4e5d71 (Git 2.42.2)\n>  #include \"strbuf.h\"\n> -||||||| 0f15832059\n> -#include \"worktree.h\"\n> -#include \"wrapper.h\"\n> -\n> -#ifdef NO_TRUSTABLE_FILEMODE\n> -#define TEST_FILEMODE 0\n> -#else\n> -#define TEST_FILEMODE 1\n> -#endif\n> -\n> -#define GIT_DEFAULT_HASH_ENVIRONMENT \"GIT_DEFAULT_HASH\"\n> -\n> -static int init_is_bare_repository = 0;\n> -static int init_shared_repository = -1;\n> -\n> -static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,\n> -\t\t\t     DIR *dir)\n> -{\n> -\tsize_t path_baselen = path->len;\n> -\tsize_t template_baselen = template_path->len;\n> -\tstruct dirent *de;\n> -\n> -\t/* Note: if \".git/hooks\" file exists in the repository being\n> -\t * re-initialized, /etc/core-git/templates/hooks/update would\n> -\t * cause \"git init\" to fail here.  I think this is sane but\n> -\t * it means that the set of templates we ship by default, along\n> -\t * with the way the namespace under .git/ is organized, should\n> -\t * be really carefully chosen.\n> -\t */\n> -\tsafe_create_dir(path->buf, 1);\n> -\twhile ((de = readdir(dir)) != NULL) {\n> -\t\tstruct stat st_git, st_template;\n> -\t\tint exists = 0;\n> -\n> -\t\tstrbuf_setlen(path, path_baselen);\n> -\t\tstrbuf_setlen(template_path, template_baselen);\n> -\n> -\t\tif (de->d_name[0] == '.')\n> -\t\t\tcontinue;\n> -\t\tstrbuf_addstr(path, de->d_name);\n> -\t\tstrbuf_addstr(template_path, de->d_name);\n> -\t\tif (lstat(path->buf, &st_git)) {\n> -\t\t\tif (errno != ENOENT)\n> -\t\t\t\tdie_errno(_(\"cannot stat '%s'\"), path->buf);\n> -\t\t}\n> -\t\telse\n> -\t\t\texists = 1;\n> -\n> -\t\tif (lstat(template_path->buf, &st_template))\n> -\t\t\tdie_errno(_(\"cannot stat template '%s'\"), template_path->buf);\n> -\n> -\t\tif (S_ISDIR(st_template.st_mode)) {\n> -\t\t\tDIR *subdir = opendir(template_path->buf);\n> -\t\t\tif (!subdir)\n> -\t\t\t\tdie_errno(_(\"cannot opendir '%s'\"), template_path->buf);\n> -\t\t\tstrbuf_addch(path, '/');\n> -\t\t\tstrbuf_addch(template_path, '/');\n> -\t\t\tcopy_templates_1(path, template_path, subdir);\n> -\t\t\tclosedir(subdir);\n> -\t\t}\n> -\t\telse if (exists)\n> -\t\t\tcontinue;\n> -\t\telse if (S_ISLNK(st_template.st_mode)) {\n> -\t\t\tstruct strbuf lnk = STRBUF_INIT;\n> -\t\t\tif (strbuf_readlink(&lnk, template_path->buf,\n> -\t\t\t\t\t    st_template.st_size) < 0)\n> -\t\t\t\tdie_errno(_(\"cannot readlink '%s'\"), template_path->buf);\n> -\t\t\tif (symlink(lnk.buf, path->buf))\n> -\t\t\t\tdie_errno(_(\"cannot symlink '%s' '%s'\"),\n> -\t\t\t\t\t  lnk.buf, path->buf);\n> -\t\t\tstrbuf_release(&lnk);\n> -\t\t}\n> -\t\telse if (S_ISREG(st_template.st_mode)) {\n> -\t\t\tif (copy_file(path->buf, template_path->buf, st_template.st_mode))\n> -\t\t\t\tdie_errno(_(\"cannot copy '%s' to '%s'\"),\n> -\t\t\t\t\t  template_path->buf, path->buf);\n> -\t\t}\n> -\t\telse\n> -\t\t\terror(_(\"ignoring template %s\"), template_path->buf);\n> -\t}\n> -}\n> -\n> -static void copy_templates(const char *option_template)\n> -{\n> -\tconst char *template_dir = get_template_dir(option_template);\n> -\tstruct strbuf path = STRBUF_INIT;\n> -\tstruct strbuf template_path = STRBUF_INIT;\n> -\tsize_t template_len;\n> -\tstruct repository_format template_format = REPOSITORY_FORMAT_INIT;\n> -\tstruct strbuf err = STRBUF_INIT;\n> -\tDIR *dir;\n> -\tchar *to_free = NULL;\n> -\n> -\tif (!template_dir || !*template_dir)\n> -\t\treturn;\n> -\n> -\tstrbuf_addstr(&template_path, template_dir);\n> -\tstrbuf_complete(&template_path, '/');\n> -\ttemplate_len = template_path.len;\n> -\n> -\tdir = opendir(template_path.buf);\n> -\tif (!dir) {\n> -\t\twarning(_(\"templates not found in %s\"), template_dir);\n> -\t\tgoto free_return;\n> -\t}\n> -\n> -\t/* Make sure that template is from the correct vintage */\n> -\tstrbuf_addstr(&template_path, \"config\");\n> -\tread_repository_format(&template_format, template_path.buf);\n> -\tstrbuf_setlen(&template_path, template_len);\n> -\n> -\t/*\n> -\t * No mention of version at all is OK, but anything else should be\n> -\t * verified.\n> -\t */\n> -\tif (template_format.version >= 0 &&\n> -\t    verify_repository_format(&template_format, &err) < 0) {\n> -\t\twarning(_(\"not copying templates from '%s': %s\"),\n> -\t\t\t  template_dir, err.buf);\n> -\t\tstrbuf_release(&err);\n> -\t\tgoto close_free_return;\n> -\t}\n> -\n> -\tstrbuf_addstr(&path, get_git_common_dir());\n> -\tstrbuf_complete(&path, '/');\n> -\tcopy_templates_1(&path, &template_path, dir);\n> -close_free_return:\n> -\tclosedir(dir);\n> -free_return:\n> -\tfree(to_free);\n> -\tstrbuf_release(&path);\n> -\tstrbuf_release(&template_path);\n> -\tclear_repository_format(&template_format);\n> -}\n> -\n> -/*\n> - * If the git_dir is not directly inside the working tree, then git will not\n> - * find it by default, and we need to set the worktree explicitly.\n> - */\n> -static int needs_work_tree_config(const char *git_dir, const char *work_tree)\n> -{\n> -\tif (!strcmp(work_tree, \"/\") && !strcmp(git_dir, \"/.git\"))\n> -\t\treturn 0;\n> -\tif (skip_prefix(git_dir, work_tree, &git_dir) &&\n> -\t    !strcmp(git_dir, \"/.git\"))\n> -\t\treturn 0;\n> -\treturn 1;\n> -}\n> -\n> -void initialize_repository_version(int hash_algo, int reinit)\n> -{\n> -\tchar repo_version_string[10];\n> -\tint repo_version = GIT_REPO_VERSION;\n> -\n> -\tif (hash_algo != GIT_HASH_SHA1)\n> -\t\trepo_version = GIT_REPO_VERSION_READ;\n> -\n> -\t/* This forces creation of new config file */\n> -\txsnprintf(repo_version_string, sizeof(repo_version_string),\n> -\t\t  \"%d\", repo_version);\n> -\tgit_config_set(\"core.repositoryformatversion\", repo_version_string);\n> -\n> -\tif (hash_algo != GIT_HASH_SHA1)\n> -\t\tgit_config_set(\"extensions.objectformat\",\n> -\t\t\t       hash_algos[hash_algo].name);\n> -\telse if (reinit)\n> -\t\tgit_config_set_gently(\"extensions.objectformat\", NULL);\n> -}\n> -\n> -static int create_default_files(const char *template_path,\n> -\t\t\t\tconst char *original_git_dir,\n> -\t\t\t\tconst char *initial_branch,\n> -\t\t\t\tconst struct repository_format *fmt,\n> -\t\t\t\tint quiet)\n> -{\n> -\tstruct stat st1;\n> -\tstruct strbuf buf = STRBUF_INIT;\n> -\tchar *path;\n> -\tchar junk[2];\n> -\tint reinit;\n> -\tint filemode;\n> -\tstruct strbuf err = STRBUF_INIT;\n> -\tconst char *work_tree = get_git_work_tree();\n> -\n> -\t/*\n> -\t * First copy the templates -- we might have the default\n> -\t * config file there, in which case we would want to read\n> -\t * from it after installing.\n> -\t *\n> -\t * Before reading that config, we also need to clear out any cached\n> -\t * values (since we've just potentially changed what's available on\n> -\t * disk).\n> -\t */\n> -\tcopy_templates(template_path);\n> -\tgit_config_clear();\n> -\treset_shared_repository();\n> -\tgit_config(git_default_config, NULL);\n> -\n> -\t/*\n> -\t * We must make sure command-line options continue to override any\n> -\t * values we might have just re-read from the config.\n> -\t */\n> -\tis_bare_repository_cfg = init_is_bare_repository || !work_tree;\n> -\tif (init_shared_repository != -1)\n> -\t\tset_shared_repository(init_shared_repository);\n> -\n> -\t/*\n> -\t * We would have created the above under user's umask -- under\n> -\t * shared-repository settings, we would need to fix them up.\n> -\t */\n> -\tif (get_shared_repository()) {\n> -\t\tadjust_shared_perm(get_git_dir());\n> -\t}\n> -\n> -\t/*\n> -\t * We need to create a \"refs\" dir in any case so that older\n> -\t * versions of git can tell that this is a repository.\n> -\t */\n> -\tsafe_create_dir(git_path(\"refs\"), 1);\n> -\tadjust_shared_perm(git_path(\"refs\"));\n> -\n> -\tif (refs_init_db(&err))\n> -\t\tdie(\"failed to set up refs db: %s\", err.buf);\n> -\n> -\t/*\n> -\t * Point the HEAD symref to the initial branch with if HEAD does\n> -\t * not yet exist.\n> -\t */\n> -\tpath = git_path_buf(&buf, \"HEAD\");\n> -\treinit = (!access(path, R_OK)\n> -\t\t  || readlink(path, junk, sizeof(junk)-1) != -1);\n> -\tif (!reinit) {\n> -\t\tchar *ref;\n> -\n> -\t\tif (!initial_branch)\n> -\t\t\tinitial_branch = git_default_branch_name(quiet);\n> -\n> -\t\tref = xstrfmt(\"refs/heads/%s\", initial_branch);\n> -\t\tif (check_refname_format(ref, 0) < 0)\n> -\t\t\tdie(_(\"invalid initial branch name: '%s'\"),\n> -\t\t\t    initial_branch);\n> -\n> -\t\tif (create_symref(\"HEAD\", ref, NULL) < 0)\n> -\t\t\texit(1);\n> -\t\tfree(ref);\n> -\t}\n> -\n> -\tinitialize_repository_version(fmt->hash_algo, 0);\n> -\n> -\t/* Check filemode trustability */\n> -\tpath = git_path_buf(&buf, \"config\");\n> -\tfilemode = TEST_FILEMODE;\n> -\tif (TEST_FILEMODE && !lstat(path, &st1)) {\n> -\t\tstruct stat st2;\n> -\t\tfilemode = (!chmod(path, st1.st_mode ^ S_IXUSR) &&\n> -\t\t\t\t!lstat(path, &st2) &&\n> -\t\t\t\tst1.st_mode != st2.st_mode &&\n> -\t\t\t\t!chmod(path, st1.st_mode));\n> -\t\tif (filemode && !reinit && (st1.st_mode & S_IXUSR))\n> -\t\t\tfilemode = 0;\n> -\t}\n> -\tgit_config_set(\"core.filemode\", filemode ? \"true\" : \"false\");\n> -\n> -\tif (is_bare_repository())\n> -\t\tgit_config_set(\"core.bare\", \"true\");\n> -\telse {\n> -\t\tgit_config_set(\"core.bare\", \"false\");\n> -\t\t/* allow template config file to override the default */\n> -\t\tif (log_all_ref_updates == LOG_REFS_UNSET)\n> -\t\t\tgit_config_set(\"core.logallrefupdates\", \"true\");\n> -\t\tif (needs_work_tree_config(original_git_dir, work_tree))\n> -\t\t\tgit_config_set(\"core.worktree\", work_tree);\n> -\t}\n> -\n> -\tif (!reinit) {\n> -\t\t/* Check if symlink is supported in the work tree */\n> -\t\tpath = git_path_buf(&buf, \"tXXXXXX\");\n> -\t\tif (!close(xmkstemp(path)) &&\n> -\t\t    !unlink(path) &&\n> -\t\t    !symlink(\"testing\", path) &&\n> -\t\t    !lstat(path, &st1) &&\n> -\t\t    S_ISLNK(st1.st_mode))\n> -\t\t\tunlink(path); /* good */\n> -\t\telse\n> -\t\t\tgit_config_set(\"core.symlinks\", \"false\");\n> -\n> -\t\t/* Check if the filesystem is case-insensitive */\n> -\t\tpath = git_path_buf(&buf, \"CoNfIg\");\n> -\t\tif (!access(path, F_OK))\n> -\t\t\tgit_config_set(\"core.ignorecase\", \"true\");\n> -\t\tprobe_utf8_pathname_composition();\n> -\t}\n> -\n> -\tstrbuf_release(&buf);\n> -\treturn reinit;\n> -}\n> -\n> -static void create_object_directory(void)\n> -{\n> -\tstruct strbuf path = STRBUF_INIT;\n> -\tsize_t baselen;\n> -\n> -\tstrbuf_addstr(&path, get_object_directory());\n> -\tbaselen = path.len;\n> -\n> -\tsafe_create_dir(path.buf, 1);\n> -\n> -\tstrbuf_setlen(&path, baselen);\n> -\tstrbuf_addstr(&path, \"/pack\");\n> -\tsafe_create_dir(path.buf, 1);\n> -\n> -\tstrbuf_setlen(&path, baselen);\n> -\tstrbuf_addstr(&path, \"/info\");\n> -\tsafe_create_dir(path.buf, 1);\n> -\n> -\tstrbuf_release(&path);\n> -}\n> -\n> -static void separate_git_dir(const char *git_dir, const char *git_link)\n> -{\n> -\tstruct stat st;\n> -\n> -\tif (!stat(git_link, &st)) {\n> -\t\tconst char *src;\n> -\n> -\t\tif (S_ISREG(st.st_mode))\n> -\t\t\tsrc = read_gitfile(git_link);\n> -\t\telse if (S_ISDIR(st.st_mode))\n> -\t\t\tsrc = git_link;\n> -\t\telse\n> -\t\t\tdie(_(\"unable to handle file type %d\"), (int)st.st_mode);\n> -\n> -\t\tif (rename(src, git_dir))\n> -\t\t\tdie_errno(_(\"unable to move %s to %s\"), src, git_dir);\n> -\t\trepair_worktrees(NULL, NULL);\n> -\t}\n> -\n> -\twrite_file(git_link, \"gitdir: %s\", git_dir);\n> -}\n> -\n> -static void validate_hash_algorithm(struct repository_format *repo_fmt, int hash)\n> -{\n> -\tconst char *env = getenv(GIT_DEFAULT_HASH_ENVIRONMENT);\n> -\t/*\n> -\t * If we already have an initialized repo, don't allow the user to\n> -\t * specify a different algorithm, as that could cause corruption.\n> -\t * Otherwise, if the user has specified one on the command line, use it.\n> -\t */\n> -\tif (repo_fmt->version >= 0 && hash != GIT_HASH_UNKNOWN && hash != repo_fmt->hash_algo)\n> -\t\tdie(_(\"attempt to reinitialize repository with different hash\"));\n> -\telse if (hash != GIT_HASH_UNKNOWN)\n> -\t\trepo_fmt->hash_algo = hash;\n> -\telse if (env) {\n> -\t\tint env_algo = hash_algo_by_name(env);\n> -\t\tif (env_algo == GIT_HASH_UNKNOWN)\n> -\t\t\tdie(_(\"unknown hash algorithm '%s'\"), env);\n> -\t\trepo_fmt->hash_algo = env_algo;\n> -\t}\n> -}\n> -\n> -int init_db(const char *git_dir, const char *real_git_dir,\n> -\t    const char *template_dir, int hash, const char *initial_branch,\n> -\t    unsigned int flags)\n> -{\n> -\tint reinit;\n> -\tint exist_ok = flags & INIT_DB_EXIST_OK;\n> -\tchar *original_git_dir = real_pathdup(git_dir, 1);\n> -\tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n> -\n> -\tif (real_git_dir) {\n> -\t\tstruct stat st;\n> -\n> -\t\tif (!exist_ok && !stat(git_dir, &st))\n> -\t\t\tdie(_(\"%s already exists\"), git_dir);\n> -\n> -\t\tif (!exist_ok && !stat(real_git_dir, &st))\n> -\t\t\tdie(_(\"%s already exists\"), real_git_dir);\n> -\n> -\t\tset_git_dir(real_git_dir, 1);\n> -\t\tgit_dir = get_git_dir();\n> -\t\tseparate_git_dir(git_dir, original_git_dir);\n> -\t}\n> -\telse {\n> -\t\tset_git_dir(git_dir, 1);\n> -\t\tgit_dir = get_git_dir();\n> -\t}\n> -\tstartup_info->have_repository = 1;\n> -\n> -\t/* Ensure `core.hidedotfiles` is processed */\n> -\tgit_config(platform_core_config, NULL);\n> -\n> -\tsafe_create_dir(git_dir, 0);\n> -\n> -\tinit_is_bare_repository = is_bare_repository();\n> -\n> -\t/* Check to see if the repository version is right.\n> -\t * Note that a newly created repository does not have\n> -\t * config file, so this will not fail.  What we are catching\n> -\t * is an attempt to reinitialize new repository with an old tool.\n> -\t */\n> -\tcheck_repository_format(&repo_fmt);\n> -\n> -\tvalidate_hash_algorithm(&repo_fmt, hash);\n> -\n> -\treinit = create_default_files(template_dir, original_git_dir,\n> -\t\t\t\t      initial_branch, &repo_fmt,\n> -\t\t\t\t      flags & INIT_DB_QUIET);\n> -\tif (reinit && initial_branch)\n> -\t\twarning(_(\"re-init: ignored --initial-branch=%s\"),\n> -\t\t\tinitial_branch);\n> -\n> -\tcreate_object_directory();\n> -\n> -\tif (get_shared_repository()) {\n> -\t\tchar buf[10];\n> -\t\t/* We do not spell \"group\" and such, so that\n> -\t\t * the configuration can be read by older version\n> -\t\t * of git. Note, we use octal numbers for new share modes,\n> -\t\t * and compatibility values for PERM_GROUP and\n> -\t\t * PERM_EVERYBODY.\n> -\t\t */\n> -\t\tif (get_shared_repository() < 0)\n> -\t\t\t/* force to the mode value */\n> -\t\t\txsnprintf(buf, sizeof(buf), \"0%o\", -get_shared_repository());\n> -\t\telse if (get_shared_repository() == PERM_GROUP)\n> -\t\t\txsnprintf(buf, sizeof(buf), \"%d\", OLD_PERM_GROUP);\n> -\t\telse if (get_shared_repository() == PERM_EVERYBODY)\n> -\t\t\txsnprintf(buf, sizeof(buf), \"%d\", OLD_PERM_EVERYBODY);\n> -\t\telse\n> -\t\t\tBUG(\"invalid value for shared_repository\");\n> -\t\tgit_config_set(\"core.sharedrepository\", buf);\n> -\t\tgit_config_set(\"receive.denyNonFastforwards\", \"true\");\n> -\t}\n> -\n> -\tif (!(flags & INIT_DB_QUIET)) {\n> -\t\tint len = strlen(git_dir);\n> -\n> -\t\tif (reinit)\n> -\t\t\tprintf(get_shared_repository()\n> -\t\t\t       ? _(\"Reinitialized existing shared Git repository in %s%s\\n\")\n> -\t\t\t       : _(\"Reinitialized existing Git repository in %s%s\\n\"),\n> -\t\t\t       git_dir, len && git_dir[len-1] != '/' ? \"/\" : \"\");\n> -\t\telse\n> -\t\t\tprintf(get_shared_repository()\n> -\t\t\t       ? _(\"Initialized empty shared Git repository in %s%s\\n\")\n> -\t\t\t       : _(\"Initialized empty Git repository in %s%s\\n\"),\n> -\t\t\t       git_dir, len && git_dir[len-1] != '/' ? \"/\" : \"\");\n> -\t}\n> -\n> -\tfree(original_git_dir);\n> -\treturn 0;\n> -}\n> -=======\n> -#include \"worktree.h\"\n> -#include \"wrapper.h\"\n> -#include \"run-command.h\"\n> -#include \"hook.h\"\n> -\n> -#ifdef NO_TRUSTABLE_FILEMODE\n> -#define TEST_FILEMODE 0\n> -#else\n> -#define TEST_FILEMODE 1\n> -#endif\n> -\n> -#define GIT_DEFAULT_HASH_ENVIRONMENT \"GIT_DEFAULT_HASH\"\n> -\n> -static int init_is_bare_repository = 0;\n> -static int init_shared_repository = -1;\n> -\n> -static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,\n> -\t\t\t     DIR *dir)\n> -{\n> -\tsize_t path_baselen = path->len;\n> -\tsize_t template_baselen = template_path->len;\n> -\tstruct dirent *de;\n> -\tint is_hooks_dir = ends_with(template_path->buf, \"/hooks/\");\n> -\n> -\t/* Note: if \".git/hooks\" file exists in the repository being\n> -\t * re-initialized, /etc/core-git/templates/hooks/update would\n> -\t * cause \"git init\" to fail here.  I think this is sane but\n> -\t * it means that the set of templates we ship by default, along\n> -\t * with the way the namespace under .git/ is organized, should\n> -\t * be really carefully chosen.\n> -\t */\n> -\tsafe_create_dir(path->buf, 1);\n> -\twhile ((de = readdir(dir)) != NULL) {\n> -\t\tstruct stat st_git, st_template;\n> -\t\tint exists = 0;\n> -\n> -\t\tstrbuf_setlen(path, path_baselen);\n> -\t\tstrbuf_setlen(template_path, template_baselen);\n> -\n> -\t\tif (de->d_name[0] == '.')\n> -\t\t\tcontinue;\n> -\t\tstrbuf_addstr(path, de->d_name);\n> -\t\tstrbuf_addstr(template_path, de->d_name);\n> -\t\tif (lstat(path->buf, &st_git)) {\n> -\t\t\tif (errno != ENOENT)\n> -\t\t\t\tdie_errno(_(\"cannot stat '%s'\"), path->buf);\n> -\t\t}\n> -\t\telse\n> -\t\t\texists = 1;\n> -\n> -\t\tif (lstat(template_path->buf, &st_template))\n> -\t\t\tdie_errno(_(\"cannot stat template '%s'\"), template_path->buf);\n> -\n> -\t\tif (S_ISDIR(st_template.st_mode)) {\n> -\t\t\tDIR *subdir = opendir(template_path->buf);\n> -\t\t\tif (!subdir)\n> -\t\t\t\tdie_errno(_(\"cannot opendir '%s'\"), template_path->buf);\n> -\t\t\tstrbuf_addch(path, '/');\n> -\t\t\tstrbuf_addch(template_path, '/');\n> -\t\t\tcopy_templates_1(path, template_path, subdir);\n> -\t\t\tclosedir(subdir);\n> -\t\t}\n> -\t\telse if (exists)\n> -\t\t\tcontinue;\n> -\t\telse if (S_ISLNK(st_template.st_mode)) {\n> -\t\t\tstruct strbuf lnk = STRBUF_INIT;\n> -\t\t\tif (strbuf_readlink(&lnk, template_path->buf,\n> -\t\t\t\t\t    st_template.st_size) < 0)\n> -\t\t\t\tdie_errno(_(\"cannot readlink '%s'\"), template_path->buf);\n> -\t\t\tif (symlink(lnk.buf, path->buf))\n> -\t\t\t\tdie_errno(_(\"cannot symlink '%s' '%s'\"),\n> -\t\t\t\t\t  lnk.buf, path->buf);\n> -\t\t\tstrbuf_release(&lnk);\n> -\t\t}\n> -\t\telse if (S_ISREG(st_template.st_mode)) {\n> -\t\t\tif (is_hooks_dir &&\n> -\t\t\t    is_executable(template_path->buf))\n> -\t\t\t\tadd_safe_hook(template_path->buf);\n> -\n> -\t\t\tif (copy_file(path->buf, template_path->buf, st_template.st_mode))\n> -\t\t\t\tdie_errno(_(\"cannot copy '%s' to '%s'\"),\n> -\t\t\t\t\t  template_path->buf, path->buf);\n> -\t\t}\n> -\t\telse\n> -\t\t\terror(_(\"ignoring template %s\"), template_path->buf);\n> -\t}\n> -}\n> -\n> -static void copy_templates(const char *option_template)\n> -{\n> -\tconst char *template_dir = get_template_dir(option_template);\n> -\tstruct strbuf path = STRBUF_INIT;\n> -\tstruct strbuf template_path = STRBUF_INIT;\n> -\tsize_t template_len;\n> -\tstruct repository_format template_format = REPOSITORY_FORMAT_INIT;\n> -\tstruct strbuf err = STRBUF_INIT;\n> -\tDIR *dir;\n> -\tchar *to_free = NULL;\n> -\n> -\tif (!template_dir || !*template_dir)\n> -\t\treturn;\n> -\n> -\tstrbuf_addstr(&template_path, template_dir);\n> -\tstrbuf_complete(&template_path, '/');\n> -\ttemplate_len = template_path.len;\n> -\n> -\tdir = opendir(template_path.buf);\n> -\tif (!dir) {\n> -\t\twarning(_(\"templates not found in %s\"), template_dir);\n> -\t\tgoto free_return;\n> -\t}\n> -\n> -\t/* Make sure that template is from the correct vintage */\n> -\tstrbuf_addstr(&template_path, \"config\");\n> -\tread_repository_format(&template_format, template_path.buf);\n> -\tstrbuf_setlen(&template_path, template_len);\n> -\n> -\t/*\n> -\t * No mention of version at all is OK, but anything else should be\n> -\t * verified.\n> -\t */\n> -\tif (template_format.version >= 0 &&\n> -\t    verify_repository_format(&template_format, &err) < 0) {\n> -\t\twarning(_(\"not copying templates from '%s': %s\"),\n> -\t\t\t  template_dir, err.buf);\n> -\t\tstrbuf_release(&err);\n> -\t\tgoto close_free_return;\n> -\t}\n> -\n> -\tstrbuf_addstr(&path, get_git_common_dir());\n> -\tstrbuf_complete(&path, '/');\n> -\tcopy_templates_1(&path, &template_path, dir);\n> -close_free_return:\n> -\tclosedir(dir);\n> -free_return:\n> -\tfree(to_free);\n> -\tstrbuf_release(&path);\n> -\tstrbuf_release(&template_path);\n> -\tclear_repository_format(&template_format);\n> -}\n> -\n> -/*\n> - * If the git_dir is not directly inside the working tree, then git will not\n> - * find it by default, and we need to set the worktree explicitly.\n> - */\n> -static int needs_work_tree_config(const char *git_dir, const char *work_tree)\n> -{\n> -\tif (!strcmp(work_tree, \"/\") && !strcmp(git_dir, \"/.git\"))\n> -\t\treturn 0;\n> -\tif (skip_prefix(git_dir, work_tree, &git_dir) &&\n> -\t    !strcmp(git_dir, \"/.git\"))\n> -\t\treturn 0;\n> -\treturn 1;\n> -}\n> -\n> -void initialize_repository_version(int hash_algo, int reinit)\n> -{\n> -\tchar repo_version_string[10];\n> -\tint repo_version = GIT_REPO_VERSION;\n> -\n> -\tif (hash_algo != GIT_HASH_SHA1)\n> -\t\trepo_version = GIT_REPO_VERSION_READ;\n> -\n> -\t/* This forces creation of new config file */\n> -\txsnprintf(repo_version_string, sizeof(repo_version_string),\n> -\t\t  \"%d\", repo_version);\n> -\tgit_config_set(\"core.repositoryformatversion\", repo_version_string);\n> -\n> -\tif (hash_algo != GIT_HASH_SHA1)\n> -\t\tgit_config_set(\"extensions.objectformat\",\n> -\t\t\t       hash_algos[hash_algo].name);\n> -\telse if (reinit)\n> -\t\tgit_config_set_gently(\"extensions.objectformat\", NULL);\n> -}\n> -\n> -static int create_default_files(const char *template_path,\n> -\t\t\t\tconst char *original_git_dir,\n> -\t\t\t\tconst char *initial_branch,\n> -\t\t\t\tconst struct repository_format *fmt,\n> -\t\t\t\tint quiet)\n> -{\n> -\tstruct stat st1;\n> -\tstruct strbuf buf = STRBUF_INIT;\n> -\tchar *path;\n> -\tchar junk[2];\n> -\tint reinit;\n> -\tint filemode;\n> -\tstruct strbuf err = STRBUF_INIT;\n> -\tconst char *work_tree = get_git_work_tree();\n> -\n> -\t/*\n> -\t * First copy the templates -- we might have the default\n> -\t * config file there, in which case we would want to read\n> -\t * from it after installing.\n> -\t *\n> -\t * Before reading that config, we also need to clear out any cached\n> -\t * values (since we've just potentially changed what's available on\n> -\t * disk).\n> -\t */\n> -\tcopy_templates(template_path);\n> -\tgit_config_clear();\n> -\treset_shared_repository();\n> -\tgit_config(git_default_config, NULL);\n> -\n> -\t/*\n> -\t * We must make sure command-line options continue to override any\n> -\t * values we might have just re-read from the config.\n> -\t */\n> -\tis_bare_repository_cfg = init_is_bare_repository || !work_tree;\n> -\tif (init_shared_repository != -1)\n> -\t\tset_shared_repository(init_shared_repository);\n> -\n> -\t/*\n> -\t * We would have created the above under user's umask -- under\n> -\t * shared-repository settings, we would need to fix them up.\n> -\t */\n> -\tif (get_shared_repository()) {\n> -\t\tadjust_shared_perm(get_git_dir());\n> -\t}\n> -\n> -\t/*\n> -\t * We need to create a \"refs\" dir in any case so that older\n> -\t * versions of git can tell that this is a repository.\n> -\t */\n> -\tsafe_create_dir(git_path(\"refs\"), 1);\n> -\tadjust_shared_perm(git_path(\"refs\"));\n> -\n> -\tif (refs_init_db(&err))\n> -\t\tdie(\"failed to set up refs db: %s\", err.buf);\n> -\n> -\t/*\n> -\t * Point the HEAD symref to the initial branch with if HEAD does\n> -\t * not yet exist.\n> -\t */\n> -\tpath = git_path_buf(&buf, \"HEAD\");\n> -\treinit = (!access(path, R_OK)\n> -\t\t  || readlink(path, junk, sizeof(junk)-1) != -1);\n> -\tif (!reinit) {\n> -\t\tchar *ref;\n> -\n> -\t\tif (!initial_branch)\n> -\t\t\tinitial_branch = git_default_branch_name(quiet);\n> -\n> -\t\tref = xstrfmt(\"refs/heads/%s\", initial_branch);\n> -\t\tif (check_refname_format(ref, 0) < 0)\n> -\t\t\tdie(_(\"invalid initial branch name: '%s'\"),\n> -\t\t\t    initial_branch);\n> -\n> -\t\tif (create_symref(\"HEAD\", ref, NULL) < 0)\n> -\t\t\texit(1);\n> -\t\tfree(ref);\n> -\t}\n> -\n> -\tinitialize_repository_version(fmt->hash_algo, 0);\n> -\n> -\t/* Check filemode trustability */\n> -\tpath = git_path_buf(&buf, \"config\");\n> -\tfilemode = TEST_FILEMODE;\n> -\tif (TEST_FILEMODE && !lstat(path, &st1)) {\n> -\t\tstruct stat st2;\n> -\t\tfilemode = (!chmod(path, st1.st_mode ^ S_IXUSR) &&\n> -\t\t\t\t!lstat(path, &st2) &&\n> -\t\t\t\tst1.st_mode != st2.st_mode &&\n> -\t\t\t\t!chmod(path, st1.st_mode));\n> -\t\tif (filemode && !reinit && (st1.st_mode & S_IXUSR))\n> -\t\t\tfilemode = 0;\n> -\t}\n> -\tgit_config_set(\"core.filemode\", filemode ? \"true\" : \"false\");\n> -\n> -\tif (is_bare_repository())\n> -\t\tgit_config_set(\"core.bare\", \"true\");\n> -\telse {\n> -\t\tgit_config_set(\"core.bare\", \"false\");\n> -\t\t/* allow template config file to override the default */\n> -\t\tif (log_all_ref_updates == LOG_REFS_UNSET)\n> -\t\t\tgit_config_set(\"core.logallrefupdates\", \"true\");\n> -\t\tif (needs_work_tree_config(original_git_dir, work_tree))\n> -\t\t\tgit_config_set(\"core.worktree\", work_tree);\n> -\t}\n> -\n> -\tif (!reinit) {\n> -\t\t/* Check if symlink is supported in the work tree */\n> -\t\tpath = git_path_buf(&buf, \"tXXXXXX\");\n> -\t\tif (!close(xmkstemp(path)) &&\n> -\t\t    !unlink(path) &&\n> -\t\t    !symlink(\"testing\", path) &&\n> -\t\t    !lstat(path, &st1) &&\n> -\t\t    S_ISLNK(st1.st_mode))\n> -\t\t\tunlink(path); /* good */\n> -\t\telse\n> -\t\t\tgit_config_set(\"core.symlinks\", \"false\");\n> -\n> -\t\t/* Check if the filesystem is case-insensitive */\n> -\t\tpath = git_path_buf(&buf, \"CoNfIg\");\n> -\t\tif (!access(path, F_OK))\n> -\t\t\tgit_config_set(\"core.ignorecase\", \"true\");\n> -\t\tprobe_utf8_pathname_composition();\n> -\t}\n> -\n> -\tstrbuf_release(&buf);\n> -\treturn reinit;\n> -}\n> -\n> -static void create_object_directory(void)\n> -{\n> -\tstruct strbuf path = STRBUF_INIT;\n> -\tsize_t baselen;\n> -\n> -\tstrbuf_addstr(&path, get_object_directory());\n> -\tbaselen = path.len;\n> -\n> -\tsafe_create_dir(path.buf, 1);\n> -\n> -\tstrbuf_setlen(&path, baselen);\n> -\tstrbuf_addstr(&path, \"/pack\");\n> -\tsafe_create_dir(path.buf, 1);\n> -\n> -\tstrbuf_setlen(&path, baselen);\n> -\tstrbuf_addstr(&path, \"/info\");\n> -\tsafe_create_dir(path.buf, 1);\n> -\n> -\tstrbuf_release(&path);\n> -}\n> -\n> -static void separate_git_dir(const char *git_dir, const char *git_link)\n> -{\n> -\tstruct stat st;\n> -\n> -\tif (!stat(git_link, &st)) {\n> -\t\tconst char *src;\n> -\n> -\t\tif (S_ISREG(st.st_mode))\n> -\t\t\tsrc = read_gitfile(git_link);\n> -\t\telse if (S_ISDIR(st.st_mode))\n> -\t\t\tsrc = git_link;\n> -\t\telse\n> -\t\t\tdie(_(\"unable to handle file type %d\"), (int)st.st_mode);\n> -\n> -\t\tif (rename(src, git_dir))\n> -\t\t\tdie_errno(_(\"unable to move %s to %s\"), src, git_dir);\n> -\t\trepair_worktrees(NULL, NULL);\n> -\t}\n> -\n> -\twrite_file(git_link, \"gitdir: %s\", git_dir);\n> -}\n> -\n> -static void validate_hash_algorithm(struct repository_format *repo_fmt, int hash)\n> -{\n> -\tconst char *env = getenv(GIT_DEFAULT_HASH_ENVIRONMENT);\n> -\t/*\n> -\t * If we already have an initialized repo, don't allow the user to\n> -\t * specify a different algorithm, as that could cause corruption.\n> -\t * Otherwise, if the user has specified one on the command line, use it.\n> -\t */\n> -\tif (repo_fmt->version >= 0 && hash != GIT_HASH_UNKNOWN && hash != repo_fmt->hash_algo)\n> -\t\tdie(_(\"attempt to reinitialize repository with different hash\"));\n> -\telse if (hash != GIT_HASH_UNKNOWN)\n> -\t\trepo_fmt->hash_algo = hash;\n> -\telse if (env) {\n> -\t\tint env_algo = hash_algo_by_name(env);\n> -\t\tif (env_algo == GIT_HASH_UNKNOWN)\n> -\t\t\tdie(_(\"unknown hash algorithm '%s'\"), env);\n> -\t\trepo_fmt->hash_algo = env_algo;\n> -\t}\n> -}\n> -\n> -int init_db(const char *git_dir, const char *real_git_dir,\n> -\t    const char *template_dir, int hash, const char *initial_branch,\n> -\t    unsigned int flags)\n> -{\n> -\tint reinit;\n> -\tint exist_ok = flags & INIT_DB_EXIST_OK;\n> -\tchar *original_git_dir = real_pathdup(git_dir, 1);\n> -\tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n> -\n> -\tif (real_git_dir) {\n> -\t\tstruct stat st;\n> -\n> -\t\tif (!exist_ok && !stat(git_dir, &st))\n> -\t\t\tdie(_(\"%s already exists\"), git_dir);\n> -\n> -\t\tif (!exist_ok && !stat(real_git_dir, &st))\n> -\t\t\tdie(_(\"%s already exists\"), real_git_dir);\n> -\n> -\t\tset_git_dir(real_git_dir, 1);\n> -\t\tgit_dir = get_git_dir();\n> -\t\tseparate_git_dir(git_dir, original_git_dir);\n> -\t}\n> -\telse {\n> -\t\tset_git_dir(git_dir, 1);\n> -\t\tgit_dir = get_git_dir();\n> -\t}\n> -\tstartup_info->have_repository = 1;\n> -\n> -\t/* Ensure `core.hidedotfiles` is processed */\n> -\tgit_config(platform_core_config, NULL);\n> -\n> -\tsafe_create_dir(git_dir, 0);\n> -\n> -\tinit_is_bare_repository = is_bare_repository();\n> -\n> -\t/* Check to see if the repository version is right.\n> -\t * Note that a newly created repository does not have\n> -\t * config file, so this will not fail.  What we are catching\n> -\t * is an attempt to reinitialize new repository with an old tool.\n> -\t */\n> -\tcheck_repository_format(&repo_fmt);\n> -\n> -\tvalidate_hash_algorithm(&repo_fmt, hash);\n> -\n> -\treinit = create_default_files(template_dir, original_git_dir,\n> -\t\t\t\t      initial_branch, &repo_fmt,\n> -\t\t\t\t      flags & INIT_DB_QUIET);\n> -\tif (reinit && initial_branch)\n> -\t\twarning(_(\"re-init: ignored --initial-branch=%s\"),\n> -\t\t\tinitial_branch);\n> -\n> -\tcreate_object_directory();\n> -\n> -\tif (get_shared_repository()) {\n> -\t\tchar buf[10];\n> -\t\t/* We do not spell \"group\" and such, so that\n> -\t\t * the configuration can be read by older version\n> -\t\t * of git. Note, we use octal numbers for new share modes,\n> -\t\t * and compatibility values for PERM_GROUP and\n> -\t\t * PERM_EVERYBODY.\n> -\t\t */\n> -\t\tif (get_shared_repository() < 0)\n> -\t\t\t/* force to the mode value */\n> -\t\t\txsnprintf(buf, sizeof(buf), \"0%o\", -get_shared_repository());\n> -\t\telse if (get_shared_repository() == PERM_GROUP)\n> -\t\t\txsnprintf(buf, sizeof(buf), \"%d\", OLD_PERM_GROUP);\n> -\t\telse if (get_shared_repository() == PERM_EVERYBODY)\n> -\t\t\txsnprintf(buf, sizeof(buf), \"%d\", OLD_PERM_EVERYBODY);\n> -\t\telse\n> -\t\t\tBUG(\"invalid value for shared_repository\");\n> -\t\tgit_config_set(\"core.sharedrepository\", buf);\n> -\t\tgit_config_set(\"receive.denyNonFastforwards\", \"true\");\n> -\t}\n> -\n> -\tif (!(flags & INIT_DB_QUIET)) {\n> -\t\tint len = strlen(git_dir);\n> -\n> -\t\tif (reinit)\n> -\t\t\tprintf(get_shared_repository()\n> -\t\t\t       ? _(\"Reinitialized existing shared Git repository in %s%s\\n\")\n> -\t\t\t       : _(\"Reinitialized existing Git repository in %s%s\\n\"),\n> -\t\t\t       git_dir, len && git_dir[len-1] != '/' ? \"/\" : \"\");\n> -\t\telse\n> -\t\t\tprintf(get_shared_repository()\n> -\t\t\t       ? _(\"Initialized empty shared Git repository in %s%s\\n\")\n> -\t\t\t       : _(\"Initialized empty Git repository in %s%s\\n\"),\n> -\t\t\t       git_dir, len && git_dir[len-1] != '/' ? \"/\" : \"\");\n> -\t}\n> -\n> -\tfree(original_git_dir);\n> -\treturn 0;\n> -}\n> ->>>>>>> c7b6b0a28f (Merge branch 'js/fix-clone-w-hooks-2.40' into HEAD)\n>\n>  static int guess_repository_type(const char *git_dir)\n>  {\n> diff --git a/config.c b/config.c\n> remerge CONFLICT (content): Merge conflict in config.c\n> index f0f09004c8..3846a37be9 100644\n> --- a/config.c\n> +++ b/config.c\n> @@ -1558,33 +1558,7 @@ static int git_default_core_config(const char *var, const char *value,\n>  \tif (!strcmp(var, \"core.attributesfile\"))\n>  \t\treturn git_config_pathname(&git_attributes_file, var, value);\n>\n> -<<<<<<< babb4e5d71 (Git 2.42.2)\n> -\tif (!strcmp(var, \"core.hookspath\")) {\n> -\t\tif (ctx->kvi && ctx->kvi->scope == CONFIG_SCOPE_LOCAL &&\n> -\t\t    git_env_bool(\"GIT_CLONE_PROTECTION_ACTIVE\", 0))\n> -\t\t\tdie(_(\"active `core.hooksPath` found in the local \"\n> -\t\t\t      \"repository config:\\n\\t%s\\nFor security \"\n> -\t\t\t      \"reasons, this is disallowed by default.\\nIf \"\n> -\t\t\t      \"this is intentional and the hook should \"\n> -\t\t\t      \"actually be run, please\\nrun the command \"\n> -\t\t\t      \"again with \"\n> -\t\t\t      \"`GIT_CLONE_PROTECTION_ACTIVE=false`\"),\n> -\t\t\t    value);\n> -||||||| 0f15832059\n> -\tif (!strcmp(var, \"core.hookspath\")) {\n> -\t\tif (current_config_scope() == CONFIG_SCOPE_LOCAL &&\n> -\t\t    git_env_bool(\"GIT_CLONE_PROTECTION_ACTIVE\", 0))\n> -\t\t\tdie(_(\"active `core.hooksPath` found in the local \"\n> -\t\t\t      \"repository config:\\n\\t%s\\nFor security \"\n> -\t\t\t      \"reasons, this is disallowed by default.\\nIf \"\n> -\t\t\t      \"this is intentional and the hook should \"\n> -\t\t\t      \"actually be run, please\\nrun the command \"\n> -\t\t\t      \"again with \"\n> -\t\t\t      \"`GIT_CLONE_PROTECTION_ACTIVE=false`\"),\n> -\t\t\t    value);\n> -=======\n>  \tif (!strcmp(var, \"core.hookspath\"))\n> ->>>>>>> c7b6b0a28f (Merge branch 'js/fix-clone-w-hooks-2.40' into HEAD)\n>  \t\treturn git_config_pathname(&git_hooks_path, var, value);\n>\n>  \tif (!strcmp(var, \"core.bare\")) {\n> diff --git a/hook.c b/hook.c\n> index 7ddb677216..7be471e335 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -59,7 +59,8 @@ void add_safe_hook(const char *path)\n>  \t}\n>  }\n>\n> -static int safe_hook_cb(const char *key, const char *value, void *d)\n> +static int safe_hook_cb(const char *key, const char *value,\n> +\t\t\tconst struct config_context *ctx UNUSED, void *d)\n>  {\n>  \tstruct strset *set = d;\n>\n> diff --git a/setup.c b/setup.c\n> index ea2aec5aaa..dc4897543e 100644\n> --- a/setup.c\n> +++ b/setup.c\n> @@ -18,6 +18,7 @@\n>  #include \"trace2.h\"\n>  #include \"worktree.h\"\n>  #include \"exec-cmd.h\"\n> +#include \"run-command.h\"\n>  #include \"hook.h\"\n>\n>  static int inside_git_dir = -1;\n> @@ -1794,6 +1795,7 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,\n>  \tsize_t path_baselen = path->len;\n>  \tsize_t template_baselen = template_path->len;\n>  \tstruct dirent *de;\n> +\tint is_hooks_dir = ends_with(template_path->buf, \"/hooks/\");\n>\n>  \t/* Note: if \".git/hooks\" file exists in the repository being\n>  \t * re-initialized, /etc/core-git/templates/hooks/update would\n> @@ -1846,6 +1848,10 @@ static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,\n>  \t\t\tstrbuf_release(&lnk);\n>  \t\t}\n>  \t\telse if (S_ISREG(st_template.st_mode)) {\n> +\t\t\tif (is_hooks_dir &&\n> +\t\t\t    is_executable(template_path->buf))\n> +\t\t\t\tadd_safe_hook(template_path->buf);\n> +\n>  \t\t\tif (copy_file(path->buf, template_path->buf, st_template.st_mode))\n>  \t\t\t\tdie_errno(_(\"cannot copy '%s' to '%s'\"),\n>  \t\t\t\t\t  template_path->buf, path->buf);\n> * js/fix-clone-w-hooks-2.43\n> diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\n> remerge CONFLICT (content): Merge conflict in t/t1800-hook.sh\n> index 1283a9bd47..c51be5f7a0 100755\n> --- a/t/t1800-hook.sh\n> +++ b/t/t1800-hook.sh\n> @@ -191,24 +191,7 @@ test_expect_success '`safe.hook.sha256` and clone protections' '\n>  \techo \"called hook\" >safe-hook.log\n>  \tEOF\n>\n> -<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 1f2e64e22d (Git 2.43.4)\n> -\tgit hook run test-hook 2>err &&\n> -\ttest_grep \"Hook ran\" err &&\n> -|||||||||||||||||||||||||||||||| babb4e5d71\n> -\tgit hook run test-hook 2>err &&\n> -\tgrep \"Hook ran\" err &&\n> -================================\n> ->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 0268cf79cb (Merge branch 'js/fix-clone-w-hooks-2.41' into HEAD)\n>  \ttest_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \\\n> -<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 1f2e64e22d (Git 2.43.4)\n> -\t\tgit hook run test-hook 2>err &&\n> -\ttest_grep \"active .core.hooksPath\" err &&\n> -\ttest_grep ! \"Hook ran\" err\n> -|||||||||||||||||||||||||||||||| babb4e5d71\n> -\t\tgit hook run test-hook 2>err &&\n> -\tgrep \"active .core.hooksPath\" err &&\n> -\t! grep \"Hook ran\" err\n> -================================\n>  \t\tgit -C safe-hook hook run pre-push 2>err &&\n>  \tcmd=\"$(grep \"git config --global --add safe.hook.sha256 [0-9a-f]\" err)\" &&\n>  \teval \"$cmd\" &&\n> @@ -235,7 +218,6 @@ test_expect_success 'Git LFS special-handling in clone protections' '\n>  \t\tgit -C lfs-hooks hook run pre-push &&\n>  \ttest_write_lines \"called pre-push\" >expect &&\n>  \ttest_cmp lfs-hooks/fake-git-lfs.log expect\n> ->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 0268cf79cb (Merge branch 'js/fix-clone-w-hooks-2.41' into HEAD)\n>  '\n>\n>  test_done\n> diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh\n> index 6ee833bac2..0fcfe6f2ce 100755\n> --- a/t/t5601-clone.sh\n> +++ b/t/t5601-clone.sh\n> @@ -835,7 +835,7 @@ test_expect_success 'clone with init.templatedir runs hooks' '\n>  \t\tgit -c init.templateDir=\"$(pwd)/tmpl\" \\\n>  \t\t\tclone --recurse-submodules \\\n>  \t\t\ttmpl/hooks hook-run-submodule 2>err &&\n> -\t\t! grep \"active .* hook found\" err &&\n> +\t\ttest_grep ! \"active .* hook found\" err &&\n>  \t\ttest_path_is_file hook-run-submodule/hook.run &&\n>  \t\ttest_path_is_file hook-run-submodule/sub/hook.run\n>  \t)\n> * js/fix-clone-w-hooks-2.44* js/fix-clone-w-hooks-2.45\n>\n"},{"id":"495042","messageId":"c201bbe3-b404-feed-fcef-8333f72068dc@gmx.de","threadId":"61509","inReplyTo":"20240518181432.GA1570600@coredump.intra.peff.net","subject":"Re: [PATCH v2 5/8] hook(clone protections): add escape hatch","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2024-05-18T19:32:07Z","receivedAt":"2024-05-18T19:32:22Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Jeff,\n\nOn Sat, 18 May 2024, Jeff King wrote:\n\n> On Sat, May 18, 2024 at 10:32:43AM +0000, Johannes Schindelin via GitGitGadget wrote:\n>\n> > To help Git LFS, and other tools behaving similarly (if there are any),\n> > let's add a new, multi-valued `safe.hook.sha256` config setting. Like\n> > the already-existing `safe.*` settings, it is ignored in\n> > repository-local configs, and it is interpreted as a list of SHA-256\n> > checksums of hooks' contents that are safe to execute during a clone\n> > operation. Future Git LFS versions will need to write those entries at\n> > the same time they install the `smudge`/`clean` filters.\n>\n> This scheme seems more complicated for the user than the sometimes\n> discussed ability to specify hook paths via config (not core.hooksPath,\n> which covers _all_ hooks, but one which allows a per-hook path).\n\nRight, it is more complicated.\n\nBut then, we are talking about `git clone` protections, as Junio points\nout, i.e. preventing hooks from running that the user did not install.\n\nGit LFS' `post-checkout` hook is an example: The user never explicitly\ninstalled this hook, and it was not there before the checkout phase of the\nclone started, yet it is there after it finished. That's the same pattern\nas many attack vectors used that we saw in the path for critical CVEs.\n\n> In either case, we're considering config to be a trusted source of\n> truth, so I think the security properties are the same. But for the\n> system here, a user updating a hook needs to do multiple steps:\n>\n>   - compute the sha256 of the hook (for which we provide no tooling\n>     support, though hopefully it is obvious how to use other tools)\n>\n>   - add the config for the sha256\n>\n>   - install the new hook into $GIT_DIR/hooks\n\nWell, there is tooling support: With the proposed patches (patch 5, to be\nprecise), Git will complain about hooks that are installed _during_ a\nclone, and then provide the following advice:\n\n\tIf this is intentional and the hook is safe to run,\n\tplease run the following command and try again:\n\n\t  git config --global --add safe.hook.sha256 <hash>\n\nWhile this won't help with the just-completed clone operation, it assists\npreventing the same issue in future clones.\n\n> Whereas if the config can just point at the hook, then there is only one\n> step: add the config for the hook (presumably pointing to a system\n> version that would have been copied into $GIT_DIR/hooks previously).\n>\n> Likewise for updates of the hooks, where the sha256 scheme requires\n> computing and adding a new hash. But when the config just points to the\n> path, there is no additional step for updating.\n>\n> In either scheme, programs like git-lfs would have to adjust to the new\n> world view. The main advantage of the sha256 scheme, it seems to me, is\n> that the baked-in sha256 values let existing versions of git-lfs work.\n> But we could also support that internally, without exposing\n> safe.hook.sha256 to the world (and thus creating an ecosystem where we\n> have to support it forever).\n>\n> Implied here is that I also think config-based hooks have a lot of\n> _other_ advantages, and so would be worth pursuing anyway, and this\n> extra safety would come along for free. I won't enumerate those\n> advantages here, but we that can be a separate discussion if need be.\n\nOne disadvantage of config-based hooks is that it is quite hard to verify\nthe provenance of the settings: Was it the user who added it, was it a\nprogram the user called, or was it exploiting a vulnerability whereby the\nconfig was written inadvertently?\n\n> And of course that feature doesn't yet exist, and is a much larger one.\n> But besides un-breaking current LFS, I'm not sure that we need to rush\n> out a more generic version of the feature.\n\nExactly. We need to unbreak Git LFS-enabled clones and release v2.45.2\nbefore I even have the head space to think more about config-based hooks.\n\nCiao,\nJohannes\n"},{"id":"495043","messageId":"20240518193528.GA1573807@coredump.intra.peff.net","threadId":"61509","inReplyTo":"xmqqmsonne6m.fsf@gitster.g","subject":"Re: [PATCH v2 5/8] hook(clone protections): add escape hatch","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-18T19:35:28Z","receivedAt":"2024-05-18T19:35:31Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Sat, May 18, 2024 at 11:54:41AM -0700, Junio C Hamano wrote:\n\n> Jeff King <peff@peff.net> writes:\n> \n> > In either case, we're considering config to be a trusted source of\n> > truth, so I think the security properties are the same. But for the\n> > system here, a user updating a hook needs to do multiple steps:\n> >\n> >   - compute the sha256 of the hook (for which we provide no tooling\n> >     support, though hopefully it is obvious how to use other tools)\n> >\n> >   - add the config for the sha256\n> >\n> >   - install the new hook into $GIT_DIR/hooks\n> \n> I am not sure why any of the above is needed.  \n> \n> Hmph.\n> \n> I was somehow (because that is how \"git config --help\" explains\n> \"safe.hook.*\") led to believe that this \"safety\" was only about \"git\n> clone would prefer not to run ANY hook before it finishes operation\n> and gives back the control to the end user, but historically it ran\n> any enabled hooks in the resulting repository that was freshly\n> created by it---so let's at least make sure the contents of the\n> hooks are known-to-be-good ones when 'git clone' runs the hooks\".\n> Most importantly, once \"git clone\" gives control back to the end\n> user and the end user had a chance to inspect the resulting\n> repository, the files in $GIT_DIR/hooks can be updated and the hooks\n> will run without incurring any cost of checking.\n> \n> Isn't that what happens?\n\nYes, sorry if I was unclear. This is _only_ about the hooks-during-clone\nsafety. So my \"a user must do this\" is really \"a user who wants a hook\nto be installed during a clone must do this\". And plausibly speaking,\nthat is mostly going to be script/program writers like git-lfs.\n\nSo the extra complexity is limited to those cases.\n\n> Looking at the control flow, hook.c:find_hook() is the one that\n> calls the function is_hook_safe_during_clone() to reject \"unsafe\"\n> ones (and allow the white-listed ones), but I do not know offhand\n> how the code limits the rejection only during clone.  So perhaps\n> this set of patches need further work to restrict the checks only to\n> \"while we are cloning\" case?\n\nI think the git_env_bool() call to check GIT_CLONE_PROTECTION_ACTIVE is\nwhat kicks in here. During non-clone calls, that will use the default of\n0.\n\n-Peff\n"},{"id":"495044","messageId":"6ad4cc61-f434-3ea4-8f54-728e6ad6b045@gmx.de","threadId":"61509","inReplyTo":"xmqqmsonne6m.fsf@gitster.g","subject":"Re: [PATCH v2 5/8] hook(clone protections): add escape hatch","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2024-05-18T19:37:50Z","receivedAt":"2024-05-18T19:38:06Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Junio,\n\nOn Sat, 18 May 2024, Junio C Hamano wrote:\n\n> Jeff King <peff@peff.net> writes:\n>\n> > In either case, we're considering config to be a trusted source of\n> > truth, so I think the security properties are the same. But for the\n> > system here, a user updating a hook needs to do multiple steps:\n> >\n> >   - compute the sha256 of the hook (for which we provide no tooling\n> >     support, though hopefully it is obvious how to use other tools)\n> >\n> >   - add the config for the sha256\n> >\n> >   - install the new hook into $GIT_DIR/hooks\n>\n> I am not sure why any of the above is needed.\n>\n> Hmph.\n>\n> I was somehow (because that is how \"git config --help\" explains\n> \"safe.hook.*\") led to believe that this \"safety\" was only about \"git\n> clone would prefer not to run ANY hook before it finishes operation\n> and gives back the control to the end user, but historically it ran\n> any enabled hooks in the resulting repository that was freshly\n> created by it---so let's at least make sure the contents of the\n> hooks are known-to-be-good ones when 'git clone' runs the hooks\".\n> Most importantly, once \"git clone\" gives control back to the end\n> user and the end user had a chance to inspect the resulting\n> repository, the files in $GIT_DIR/hooks can be updated and the hooks\n> will run without incurring any cost of checking.\n>\n> Isn't that what happens?\n>\n> Looking at the control flow, hook.c:find_hook() is the one that\n> calls the function is_hook_safe_during_clone() to reject \"unsafe\"\n> ones (and allow the white-listed ones), but I do not know offhand\n> how the code limits the rejection only during clone.  So perhaps\n> this set of patches need further work to restrict the checks only to\n> \"while we are cloning\" case?\n\nThe logic in `find_hook()` reads like this:\n\n        if (!git_hooks_path && git_env_bool(\"GIT_CLONE_PROTECTION_ACTIVE\", 0) &&\n            !is_hook_safe_during_clone(name, path.buf, sha256))\n                die(_(\"active `%s` hook found during `git clone`:\\n\\t%s\\n\"\n                      \"For security reasons, this is disallowed by default.\\n\"\n                      \"If this is intentional and the hook is safe to run, \"\n                      \"please run the following command and try again:\\n\\n\"\n                      \"  git config --global --add safe.hook.sha256 %s\"),\n                    name, path.buf, sha256);\n\nThe `!git_hooks_path` accounts for the fact that users can choose to set\nthe `core.hooksPath` in their global configs, in which case `git clone`\n_should_ expect hooks to be present that do not originate from Git's\ntemplates.\n\nThe `GIT_CLONE_PROTECTION_ACTIVE` check is the one that limits the\nrejection to only happen during a clone: This environment variable is set\nin `git clone` (carefully passing 0 as `overwrite` parameter to `setenv()`\nto allow users to override this protection).\n\nThe reason why it has to be done via an environment variable is that `git\nclone` can spawn many processes that all need to respect this protection,\nmost notably when a recursive clone calls `git submodule--helper update`.\n\nCiao,\nJohannes\n"},{"id":"495046","messageId":"20240518194724.GB1573807@coredump.intra.peff.net","threadId":"61509","inReplyTo":"c201bbe3-b404-feed-fcef-8333f72068dc@gmx.de","subject":"Re: [PATCH v2 5/8] hook(clone protections): add escape hatch","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-18T19:47:24Z","receivedAt":"2024-05-18T19:47:25Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Sat, May 18, 2024 at 09:32:07PM +0200, Johannes Schindelin wrote:\n\n> >   - compute the sha256 of the hook (for which we provide no tooling\n> >     support, though hopefully it is obvious how to use other tools)\n> [...]\n> \n> Well, there is tooling support: With the proposed patches (patch 5, to be\n> precise), Git will complain about hooks that are installed _during_ a\n> clone, and then provide the following advice:\n> \n> \tIf this is intentional and the hook is safe to run,\n> \tplease run the following command and try again:\n> \n> \t  git config --global --add safe.hook.sha256 <hash>\n> \n> While this won't help with the just-completed clone operation, it assists\n> preventing the same issue in future clones.\n\nWhat I meant by tooling support was: how do you find out the sha256 hash\nof the hook you're wanting to bless?\n\nI'd imagine you'd reach for the stand-alone \"sha256\" tool. But there is\nno Git tool to compute the hash (you can't use any of the usual tools\nlike \"hash-object\" because it is not a pure hash of the content). Should\nwe provide one? Or at least tell the user which third-party command is\nlikely to be used?\n\n> > Implied here is that I also think config-based hooks have a lot of\n> > _other_ advantages, and so would be worth pursuing anyway, and this\n> > extra safety would come along for free. I won't enumerate those\n> > advantages here, but we that can be a separate discussion if need be.\n> \n> One disadvantage of config-based hooks is that it is quite hard to verify\n> the provenance of the settings: Was it the user who added it, was it a\n> program the user called, or was it exploiting a vulnerability whereby the\n> config was written inadvertently?\n\nBut isn't that true of the safe.hook.sha256 value, too? If I can attack\n.git/config, then I can set it to match the attack hook (not to mention\nthe zillion other config options which execute arbitrary code).\n\nIf we really want to harden .git against attacks which can overwrite\nfiles in it, then I think the long-term path may be something like:\n\n  - add support for specifying hooks via config. Leave .git/hooks for\n    compatibility.\n\n  - introduce a config option to disable .git/hooks support. Respect it\n    only outside of .git/config. Default to false to start for backwards\n    compatibility. Eventually flip it to true by default.\n\nAnd then perhaps something similar for in-repo config (add an option to\ndisable in-repo config except for repos marked as safe).\n\n> > And of course that feature doesn't yet exist, and is a much larger one.\n> > But besides un-breaking current LFS, I'm not sure that we need to rush\n> > out a more generic version of the feature.\n> \n> Exactly. We need to unbreak Git LFS-enabled clones and release v2.45.2\n> before I even have the head space to think more about config-based hooks.\n\nTo be clear, I'm not proposing doing nothing. I'm proposing un-breaking\nLFS either by rolling back the defense-in-depth or adding hard-coded\nhashes, neither of which introduces a user-visible feature that must be\nsupported. And then proceed with new features in the regular cycle.\n\nThe hard-coded hashes are obviously a ticking time bomb until lfs\nupdates again (and they don't help any as-yet-unknown program which does\nthe same thing). So I'd suggest just rolling back the feature entirely\nin the meantime.\n\n-Peff\n"},{"id":"495048","messageId":"86d57213-e3b2-c985-6d69-71568c66fc9c@gmx.de","threadId":"61509","inReplyTo":"20240518194724.GB1573807@coredump.intra.peff.net","subject":"Re: [PATCH v2 5/8] hook(clone protections): add escape hatch","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2024-05-18T20:06:36Z","receivedAt":"2024-05-18T20:06:50Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Jeff,\n\nOn Sat, 18 May 2024, Jeff King wrote:\n\n> On Sat, May 18, 2024 at 09:32:07PM +0200, Johannes Schindelin wrote:\n>\n> > > Implied here is that I also think config-based hooks have a lot of\n> > > _other_ advantages, and so would be worth pursuing anyway, and this\n> > > extra safety would come along for free. I won't enumerate those\n> > > advantages here, but we that can be a separate discussion if need\n> > > be.\n> >\n> > One disadvantage of config-based hooks is that it is quite hard to\n> > verify the provenance of the settings: Was it the user who added it,\n> > was it a program the user called, or was it exploiting a vulnerability\n> > whereby the config was written inadvertently?\n>\n> But isn't that true of the safe.hook.sha256 value, too?\n\nNo, because `safe.hook.sha256` (like `safe.directory` and\n`safe.bareRepository`) is only respected in \"protected\" configs, i.e.\nsystem-wide, user-wide and command-line config. Any such settings in the\nrepository-local config are ignored.\n\n> If I can attack .git/config, then I can set it to match the attack hook\n> (not to mention the zillion other config options which execute arbitrary\n> code).\n>\n> If we really want to harden .git against attacks which can overwrite\n> files in it, then I think the long-term path may be something like:\n>\n>   - add support for specifying hooks via config. Leave .git/hooks for\n>     compatibility.\n>\n>   - introduce a config option to disable .git/hooks support. Respect it\n>     only outside of .git/config. Default to false to start for backwards\n>     compatibility. Eventually flip it to true by default.\n>\n> And then perhaps something similar for in-repo config (add an option to\n> disable in-repo config except for repos marked as safe).\n>\n> > > And of course that feature doesn't yet exist, and is a much larger one.\n> > > But besides un-breaking current LFS, I'm not sure that we need to rush\n> > > out a more generic version of the feature.\n> >\n> > Exactly. We need to unbreak Git LFS-enabled clones and release v2.45.2\n> > before I even have the head space to think more about config-based hooks.\n>\n> To be clear, I'm not proposing doing nothing. I'm proposing un-breaking\n> LFS either by rolling back the defense-in-depth or adding hard-coded\n> hashes, neither of which introduces a user-visible feature that must be\n> supported. And then proceed with new features in the regular cycle.\n>\n> The hard-coded hashes are obviously a ticking time bomb until lfs\n> updates again (and they don't help any as-yet-unknown program which does\n> the same thing). So I'd suggest just rolling back the feature entirely\n> in the meantime.\n\nRolling back the defense-in-depth would be a mistake: We do see (seemingly\non a yearly cadence) reports of vulnerabilities in Git that often raise to\ncritical severity by exploiting the hooks feature (typically in\nconjunction with submodules). There is no reason to believe that this\nsteady trickle will stop any time soon. The defense-in-depth we introduced\nwould stop at least that escalation path that turns those vulnerabilities\ninto critical attack vectors putting users at risk.\n\nEven worse: If we removed these protections without any replacement, now\nwe basically told hackers where to look for nice, exploitable bugs,\npublicly.\n\nFor what it's worth, I was originally also in favor of the pretty surgical\naddition of the hard-coded hashes specifically to unbreak Git LFS-enabled\nclones. You must have seen my proposal that I sent to the Git security\nmailing list.\n\nHowever, brian suggested that Git LFS may not be the only 3rd-party\napplication that is affected by the clone protections. I have my doubts\nthat other applications use a similar route, it strikes me as quite hacky\nto install a hook while running a `smudge` filter, yet I do admit that\nthere is a possibility. Which is why we introduced the `safe.hooks.sha256`\nsettings.\n\nThis strikes a good balance between unbreaking Git LFS and still\nbenefitting from the defense-in-depth that helps fend off future critical\nvulnerabilities.\n\nIf we did not have such a balanced way to address the Git LFS breakage, I\nwould totally agree with you that we would need to consider rolling back\nthe defense-in-depth. Happily, we don't have to.\n\nCiao,\nJohannes\n\nP.S.: For what it's worth, the pattern we see in Git LFS is relatively\nhard to replicate. `git clone` does not offer any easy and convenient way\nto install hooks during the operation other than via templates (which,\nunlike Git LFS-enabled clones, is _not_ broken in v2.45.1). Of course,\nusers could start a clone and then manually copy a `post-checkout` hook\ninto `.git/hooks/` _while the clone is still running_. I kind of doubt\nthat that's common practice we need to support, though.\n"},{"id":"495049","messageId":"c3c2e460-e992-d7f9-754c-3c8c0aaca488@gmx.de","threadId":"61509","inReplyTo":"c67780c5-ab50-37f0-a507-91e8b721b349@gmx.de","subject":"Re: [PATCH v2 0/8] Various fixes for v2.45.1 and friends","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2024-05-18T20:13:34Z","receivedAt":"2024-05-18T20:13:48Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Junio,\n\nOn Sat, 18 May 2024, Johannes Schindelin wrote:\n\n> On Sat, 18 May 2024, Junio C Hamano wrote:\n>\n> > I have applied this to maint-2.39 and then merged them up to the\n> > maintenance tracks.  The results will be pushed out to the \"split\n> > out\" repository at\n> >\n> >     https://github.com/gitster/git/\n> >\n> > as these branches:\n> >\n> >     js/fix-clone-w-hooks-2.39\n> >     js/fix-clone-w-hooks-2.40\n> >     js/fix-clone-w-hooks-2.41\n> >     js/fix-clone-w-hooks-2.42\n> >     js/fix-clone-w-hooks-2.43\n> >     js/fix-clone-w-hooks-2.44\n> >     js/fix-clone-w-hooks-2.45\n>\n> Thank you!\n>\n> As it happens, I had already worked on tentative/maint-* branches (that\n> you can see here: https://github.com/dscho/git/branches/active), but\n> had to take a break before finalizing them.\n>\n> The major difference I see is that js/fix-clone-w-hooks-2.41 still\n> declares `do_files_match()` in `setup.h` (even if it is no longer defined\n> or called), and that `hook.c` includes `copy.h` (but that is no longer\n> needed), and that `setup.h` includes `hook.h` (but that's not needed\n> either).\n>\n> While comparing, I noticed that I had missed an extra empty line in my\n> merge conflict resolutions, a `grep` -> `test_grep` conversation and an\n> `UNUSED` attribute for the `ctx` parameter of the `safe_hook_cb()`\n> function. So I am really grateful that you did those integrations\n> independently.\n>\n> You will also note that I tentatively added commits to mark v2.39.5, ...,\n> v2.45.2 ready for tagging, along with release notes, just so we can hit\n> the ground running as soon as reviews settle down.\n\nOh, I forgot to mention that I also integrated backports of some CI fixes\ninto the `tentative/maint-*` branches, to let the `osx-gcc` job pass\nagain.\n\nCiao,\nJohannes\n>\n"},{"id":"495050","messageId":"20240518211224.GA1574761@coredump.intra.peff.net","threadId":"61509","inReplyTo":"86d57213-e3b2-c985-6d69-71568c66fc9c@gmx.de","subject":"Re: [PATCH v2 5/8] hook(clone protections): add escape hatch","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2024-05-18T21:12:24Z","receivedAt":"2024-05-18T21:12:26Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Sat, May 18, 2024 at 10:06:36PM +0200, Johannes Schindelin wrote:\n\n> > > One disadvantage of config-based hooks is that it is quite hard to\n> > > verify the provenance of the settings: Was it the user who added it,\n> > > was it a program the user called, or was it exploiting a vulnerability\n> > > whereby the config was written inadvertently?\n> >\n> > But isn't that true of the safe.hook.sha256 value, too?\n> \n> No, because `safe.hook.sha256` (like `safe.directory` and\n> `safe.bareRepository`) is only respected in \"protected\" configs, i.e.\n> system-wide, user-wide and command-line config. Any such settings in the\n> repository-local config are ignored.\n\nAh, true. I think the issue still holds for all of the other config that\nruns arbitrary code, though, doesn't it?\n\n> Rolling back the defense-in-depth would be a mistake: We do see (seemingly\n> on a yearly cadence) reports of vulnerabilities in Git that often raise to\n> critical severity by exploiting the hooks feature (typically in\n> conjunction with submodules). There is no reason to believe that this\n> steady trickle will stop any time soon. The defense-in-depth we introduced\n> would stop at least that escalation path that turns those vulnerabilities\n> into critical attack vectors putting users at risk.\n\nMost of the vulnerabilities that I recall could just as easily write\nover .git/config. But I didn't catalog them. Do you have specific ones\nin mind?\n\n> Even worse: If we removed these protections without any replacement, now\n> we basically told hackers where to look for nice, exploitable bugs,\n> publicly.\n\nI don't find this line of reasoning all that compelling. The existing\nvulnerabilities that led you to the defense-in-depth protection already\npointed them in the right direction.\n\nSo I'm not convinced that temporarily moving back to the v2.45.0 state\nis all that dangerous. If it were a known vulnerability, yes, I'd worry.\nFor defense-in-depth, less so.\n\n> For what it's worth, I was originally also in favor of the pretty surgical\n> addition of the hard-coded hashes specifically to unbreak Git LFS-enabled\n> clones. You must have seen my proposal that I sent to the Git security\n> mailing list.\n> \n> However, brian suggested that Git LFS may not be the only 3rd-party\n> application that is affected by the clone protections. I have my doubts\n> that other applications use a similar route, it strikes me as quite hacky\n> to install a hook while running a `smudge` filter, yet I do admit that\n> there is a possibility. Which is why we introduced the `safe.hooks.sha256`\n> settings.\n> \n> This strikes a good balance between unbreaking Git LFS and still\n> benefitting from the defense-in-depth that helps fend off future critical\n> vulnerabilities.\n> \n> If we did not have such a balanced way to address the Git LFS breakage, I\n> would totally agree with you that we would need to consider rolling back\n> the defense-in-depth. Happily, we don't have to.\n\nMy main complaint is that it introduces a confusing and complicated\nrequirement that LFS (and maybe others) have to think about in\nperpetuity. And we may end up with a better solution. We got bit by\npushing out the v2.45.1 change without a lot of end-user testing. Now it\nseems like v2.45.2 is being rushed out to fix it. It would hopefully see\n_more_ testing, as it's being done in the open.\n\nBut it sounds like we're throwing away our usual release-engineering\npractices (where the usual practice for a regression is \"revert it, it\ncan happen in the next cycle\") in favor of a security fix. Again, for a\nvulnerability fix, that makes sense. But for layered defense, I find it\nless compelling.\n\nAnyway, I have said my piece and I don't think I have much to add. So\neither you agree or not, and if this is the direction the project wants\nto go, I won't object further.\n\n> P.S.: For what it's worth, the pattern we see in Git LFS is relatively\n> hard to replicate. `git clone` does not offer any easy and convenient way\n> to install hooks during the operation other than via templates (which,\n> unlike Git LFS-enabled clones, is _not_ broken in v2.45.1). Of course,\n> users could start a clone and then manually copy a `post-checkout` hook\n> into `.git/hooks/` _while the clone is still running_. I kind of doubt\n> that that's common practice we need to support, though.\n\nI think we've run into similar issues with remote helpers, which run\narbitrary code and could install hooks. The recent one I'm thinking of\nis:\n\n  https://lore.kernel.org/git/20240503020432.2fxwuhjsvumy7i7z@glandium.org/\n\nthough that wasn't a hook problem, but rather leaving the repo in an\nuninitialized state for longer.\n\n-Peff\n"},{"id":"495051","messageId":"xmqqed9yob4v.fsf@gitster.g","threadId":"61509","inReplyTo":"20240518211224.GA1574761@coredump.intra.peff.net","subject":"Re: [PATCH v2 5/8] hook(clone protections): add escape hatch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-19T01:15:12Z","receivedAt":"2024-05-19T01:15:21Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> But it sounds like we're throwing away our usual release-engineering\n> practices (where the usual practice for a regression is \"revert it, it\n> can happen in the next cycle\") in favor of a security fix. Again, for a\n> vulnerability fix, that makes sense. But for layered defense, I find it\n> less compelling.\n\nI find it a lot less compelling, too.\n\nIt unfortunately involves about the same amount of conflict\nmanagement to do the (partial) revert for all these maintenance\ntracks as it would then later take a \"fix in the next cycle\" for all\nthese tracks, which made me feel somewhat hesitant.\n\nBut considering that we are not talking about lifting vulnerability\nfix, it may make sense to do the (partial) revert all the way down\nto 2.39 track but do the \"fix in the next cycle\" only for 2.45 and\nlater (or even in 2.46 only, without even aiming to touch 2.45\ntrack).\n\nThanks for a dose of sanity.\n\n"},{"id":"495081","messageId":"5e0d660a-b1ee-2202-752c-d46d3b0c8a19@gmx.de","threadId":"61509","inReplyTo":"xmqqed9yob4v.fsf@gitster.g","subject":"Re: [PATCH v2 5/8] hook(clone protections): add escape hatch","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2024-05-20T16:05:30Z","receivedAt":"2024-05-20T16:05:56Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi,\n\nOn Sat, 18 May 2024, Junio C Hamano wrote:\n\n> But considering that we are not talking about lifting vulnerability fix,\n> it may make sense to do the (partial) revert all the way down to 2.39\n> track but do the \"fix in the next cycle\" only for 2.45 and later (or\n> even in 2.46 only, without even aiming to touch 2.45 track).\n\nTo ensure that I don't misunderstand you: You are talking about reverting\n8db1e8743c0 (clone: prevent hooks from running during a clone,\n2024-03-28), right?\n\nIt is _technically_ true that this is not a vulnerability fix. But only\n_technically_. Practically, it is preventing vulnerabilities from reaching\nthe critical level.\n\nLet's take the most recently-addressed critical vulnerability,\nCVE-2024-32002. I carefully crafted above-mentioned commit such that it\nwould have prevent the Remote Code Execution attack vector noted in that\nCVE.\n\nTo put this into perspective: If this protection had been put in place\nbefore v2.39.4, the CVSS score of CVS-2024-32002 would not have been\n9.1 (Critical), but instead 2.2 (Low).\n\nIn other words, even if a vulnerability was found, Git's users would have\nbeen safer with this defense-in-depth in place.\n\nThe same `post-checkout` hook attack vector likewise raised the severity\nof CVE-2021-21300, CVE-2019-1354, CVE-2019-1353, CVE-2019-1352, and\nCVE-2019-1349.\n\nIt also raised the severity of the vulnerability fixed in v1.8.5.6 (for\nwhich we did not obtain a CVE).\n\nBased on past experience, we must expect the semi-steady trickle of Git\nvulnerabilities to continue, and having this defense-in-depth in place\nwill invaluable in helping to keep the severity of those future\nvulnerabilities low.\n\nTherefore reverting that commit would put Git's users at risk.\n\nIn combination with the fact that we have a path forward via the patches\nthat are under discussion in this here mail thread, a path forward that\navoids that risk and incurs an acceptable cost [*1*], the plan to revert\n8db1e8743c0 instead should be questioned.\n\nCiao,\nJohannes\n\nP.S.: A concern was raised about `safe.hook.sha256` not having tooling to\ngenerate those SHA-256 checksums, and putting a burden on 3rd-party tool\ndevelopers.\n\nHowever, tooling to generate SHA-256 checksums of files is ubiquitous,\nthere is `sha256sum` and `openssl dgst -sha256`, just to name two tools\nthat are widely available.\n\nAnd it's not as if _Git users_ would have to generate those checksums. It\nwould be a one-time cost for the developers whose tools install those\nhooks during a clone (Git offers no option to install hooks during cloning\nother than templates, which is not broken in v2.39.4, ..., v2.45.1).\n\nWhich brings me to that mysterious mention of tools other than Git LFS\nbeing potentially affected. It is quite dubitable that tools other than\nGit LFS use this method of changing the Git repository configuration in\nsuch a major way as to install hooks _while running a `smudge` filter_.\n\nIt must be put into doubt, too, that this method of abusing the `smudge`\nfilter is the best design to address Git LFS' needs. A serious downside,\nfor example, is that this Git LFS strategy is in conflict with\nuser-provided `post-checkout` hooks that are copied via templates. A\nbetter design that comes immediately to mind would be to add a new,\ngeneric batched-smudge filter that Git LFS could use, and that would be\nconfigured in the system or user-wide config just like Git LFS'\n`smudge`/`clean` filters, without the need to play hacky games during a\nclone operation that are very similar to malicious attacks' strategies to\nabuse Git's hook feature to allow Remote Code Execution.\n"},{"id":"495096","messageId":"xmqqzfskfiu3.fsf@gitster.g","threadId":"61509","inReplyTo":"5e0d660a-b1ee-2202-752c-d46d3b0c8a19@gmx.de","subject":"Re: [PATCH v2 5/8] hook(clone protections): add escape hatch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-20T18:18:12Z","receivedAt":"2024-05-20T18:18:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n\n> To put this into perspective: If this protection had been put in place\n> before v2.39.4, the CVSS score of CVS-2024-32002 would not have been\n> 9.1 (Critical), but instead 2.2 (Low).\n\nBut we wouldn't have a working git-lfs then, so that comparison is\nnot quite fair.  As brian already said, you can reduce the score by\nmaking Git do nothing, which is _also_ an absurd position to take\n\"security\" (in air quotes) over everything else like usability and\nfunctionality.  And this time, the layered security went a bit too\naggressive.\n\nAlso as Peff said and I agreed to, we are not talking about refusing\nto do anything on top.  It was just that the \"never run any approved\nhook during clone\" turned out to be not-quite-fully thought out and\nit should be reworked in the open, and reverting that wholesale\nwould hopefully give us a cleaner ground to design it.\n\nThe end-result of such a reworking in the open may turn out to be\nthe same (or similar) \"register the blob object name of the contents\nto appear in approved hook scripts\", or it may look completely\ndifferent.  But the road to get there, and the state of the system\nwhile we get there, would be different.\n\nI would probably see if I can take brian's revert directly; if it\napplies to the oldest maint-2.39 track, it would be the ideal, but\nwe'd still need to prepare a similar 7-track cascade like we did for\nthe js/fix-clone-w-hooks-2.XX topics.  If it is only for the master,\nit needs to be munged to apply to maint-2.39 first.\n\nThanks.\n"},{"id":"495107","messageId":"736b43a1-a371-4d6f-7739-b5b64e060585@gmx.de","threadId":"61509","inReplyTo":"xmqqzfskfiu3.fsf@gitster.g","subject":"Re: [PATCH v2 5/8] hook(clone protections): add escape hatch","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2024-05-20T19:38:02Z","receivedAt":"2024-05-20T19:38:22Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Junio,\n\nOn Mon, 20 May 2024, Junio C Hamano wrote:\n\n> Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n>\n> > To put this into perspective: If this protection had been put in place\n> > before v2.39.4, the CVSS score of CVS-2024-32002 would not have been\n> > 9.1 (Critical), but instead 2.2 (Low).\n>\n> But we wouldn't have a working git-lfs then, so that comparison is\n> not quite fair.\n\nI obviously did not mean to break Git LFS. And obviously I did not mean to\ninsist on the current clone protection if we could not have fixed Git LFS.\nBut we have patches in hand for that.\n\n> As brian already said, you can reduce the score by making Git do\n> nothing, which is _also_ an absurd position to take \"security\" (in air\n> quotes) over everything else like usability and functionality.  And this\n> time, the layered security went a bit too aggressive.\n\nRight. And I never said that we should do something as absurd, so I fail\nto see your point.\n\n> Also as Peff said and I agreed to, we are not talking about refusing\n> to do anything on top.  It was just that the \"never run any approved\n> hook during clone\" turned out to be not-quite-fully thought out and\n> it should be reworked in the open, and reverting that wholesale\n> would hopefully give us a cleaner ground to design it.\n>\n> The end-result of such a reworking in the open may turn out to be\n> the same (or similar) \"register the blob object name of the contents\n> to appear in approved hook scripts\", or it may look completely\n> different.  But the road to get there, and the state of the system\n> while we get there, would be different.\n\nI see there is no convincing you that the difference to our regular\n\"revert-then-redesign-in-the-open\" process is that we're talking about\nsomething security-relevant here, and that the revert should hence not be\ndone lightly.\n\nI've made my position clear, so have you. Since you have the last say,\nit's what you say that goes.\n\nLet me quickly iterate on this here patch series (as well as the\n`tentative/maint-*` branches) so that we can accelerate toward a fixed\nversion again; Git LFS has been broken for long enough, I'd think.\n\nCiao,\nJohannes\n\n> I would probably see if I can take brian's revert directly; if it\n> applies to the oldest maint-2.39 track, it would be the ideal, but\n> we'd still need to prepare a similar 7-track cascade like we did for\n> the js/fix-clone-w-hooks-2.XX topics.  If it is only for the master,\n> it needs to be munged to apply to maint-2.39 first.\n>\n> Thanks.\n>\n"},{"id":"495109","messageId":"xmqq1q5wfdsq.fsf@gitster.g","threadId":"61509","inReplyTo":"736b43a1-a371-4d6f-7739-b5b64e060585@gmx.de","subject":"Re: [PATCH v2 5/8] hook(clone protections): add escape hatch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-20T20:07:01Z","receivedAt":"2024-05-20T20:07:04Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:\n\n>> As brian already said, you can reduce the score by making Git do\n>> nothing, which is _also_ an absurd position to take \"security\" (in air\n>> quotes) over everything else like usability and functionality.  And this\n>> time, the layered security went a bit too aggressive.\n>\n> Right. And I never said that we should do something as absurd, so I fail\n> to see your point.\n\nIt went a bit too aggressive, closer to an absurd version of Git\nthat does nothing, for users of git-lfs and the hooksdir config.\nLuckily these two were reported/found soon enough but we do not know\nwhat other fallouts remain.\n\n> Let me quickly iterate on this here patch series (as well as the\n> `tentative/maint-*` branches) so that we can accelerate toward a fixed\n> version again; Git LFS has been broken for long enough, I'd think.\n\nIt would be nice to go back to the pre-2.39.4 state so that we\ncan redo it from the clean slate soon.\n\nThanks.\n"},{"id":"495110","messageId":"d4a003bf2ceafcc6d47d01d21b7faff48c9e85aa.1716236526.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.v3.git.1716236526.gitgitgadget@gmail.com","subject":"[PATCH v3 1/6] hook: plug a new memory leak","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-20T20:22:00Z","receivedAt":"2024-05-20T20:22:11Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nIn 8db1e8743c0 (clone: prevent hooks from running during a clone,\n2024-03-28), I introduced an inadvertent memory leak that was\nunfortunately not caught before v2.45.1 was released. Here is a fix.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n hook.c | 4 +++-\n 1 file changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/hook.c b/hook.c\nindex 632b537b993..fc974cee1d8 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -18,8 +18,10 @@ static int identical_to_template_hook(const char *name, const char *path)\n \t\tfound_template_hook = access(template_path.buf, X_OK) >= 0;\n \t}\n #endif\n-\tif (!found_template_hook)\n+\tif (!found_template_hook) {\n+\t\tstrbuf_release(&template_path);\n \t\treturn 0;\n+\t}\n \n \tret = do_files_match(template_path.buf, path);\n \n-- \ngitgitgadget\n\n"},{"id":"495111","messageId":"pull.1732.v3.git.1716236526.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.v2.git.1716028366.gitgitgadget@gmail.com","subject":"[PATCH v3 0/6] Various fixes for v2.45.1 and friends","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-20T20:21:59Z","receivedAt":"2024-05-20T20:22:11Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"There have been a couple of issues that were reported about v2.45.1, and in\naddition I have noticed some myself:\n\n * a memory leak in the clone protection logic\n * a missed adjustment in the Makefile that leads to an incorrect templates\n   path in v2.39.4, v2.40.2 and v2.41.1 (but not in v2.42.2, ..., v2.45.1)\n * an overzealous core.hooksPath check\n * that Git LFS clone problem where it exits with an error (even if the\n   clone often succeeded...)\n\nThis patch series is based on maint-2.39 to allow for (relatively) easy\nfollow-up versions v2.39.5, ..., v2.45.2.\n\nChanges since v2:\n\n * instead of introducing an escape hatch for the clone protections and\n   special-casing Git LFS, drop the clone protections\n\nChanges since v1:\n\n * simplified adding the SHA-256s corresponding to Git LFS' hooks\n * the core.hooksPath test case now verifies that the config setting was\n   configured correctly\n\nJohannes Schindelin (6):\n  hook: plug a new memory leak\n  init: use the correct path of the templates directory again\n  Revert \"core.hooksPath: add some protection while cloning\"\n  tests: verify that `clone -c core.hooksPath=/dev/null` works again\n  clone: drop the protections where hooks aren't run\n  Revert \"Add a helper function to compare file contents\"\n\n Makefile                     |  2 +-\n builtin/clone.c              | 12 +-------\n cache.h                      | 14 ---------\n config.c                     | 13 +-------\n copy.c                       | 58 ------------------------------------\n hook.c                       | 32 --------------------\n t/helper/test-path-utils.c   | 10 -------\n t/t0060-path-utils.sh        | 41 -------------------------\n t/t1350-config-hooks-path.sh |  7 +++++\n t/t1800-hook.sh              | 15 ----------\n t/t5601-clone.sh             | 51 -------------------------------\n 11 files changed, 10 insertions(+), 245 deletions(-)\n\n\nbase-commit: 47b6d90e91835082010da926f6a844d4441c57a6\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-1732%2Fdscho%2Fvarious-fixes-for-v2.45.1-and-friends-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1732/dscho/various-fixes-for-v2.45.1-and-friends-v3\nPull-Request: https://github.com/gitgitgadget/git/pull/1732\n\nRange-diff vs v2:\n\n 1:  d4a003bf2ce = 1:  d4a003bf2ce hook: plug a new memory leak\n 2:  961dfc35f42 = 2:  961dfc35f42 init: use the correct path of the templates directory again\n 3:  57db89a1497 = 3:  57db89a1497 Revert \"core.hooksPath: add some protection while cloning\"\n 4:  cd14042b065 = 4:  cd14042b065 tests: verify that `clone -c core.hooksPath=/dev/null` works again\n 5:  b841db8392e < -:  ----------- hook(clone protections): add escape hatch\n 6:  5e5128bc232 < -:  ----------- hooks(clone protections): special-case current Git LFS hooks\n 7:  bd6d72625f5 ! 5:  0044a355674 hooks(clone protections): simplify templates hooks validation\n     @@ Metadata\n      Author: Johannes Schindelin <Johannes.Schindelin@gmx.de>\n      \n       ## Commit message ##\n     -    hooks(clone protections): simplify templates hooks validation\n     +    clone: drop the protections where hooks aren't run\n      \n     -    When an active hook is encountered during a clone operation, to protect\n     -    against Remote Code Execution attack vectors, Git checks whether the\n     -    hook was copied over from the templates directory.\n     +    As part of the security bug-fix releases v2.39.4, ..., v2.45.1, I\n     +    introduced logic to safeguard `git clone` from running hooks that were\n     +    installed _during_ the clone operation.\n      \n     -    When that logic was introduced, there was no other way to check this\n     -    than to add a function to compare files.\n     +    The rationale was that Git's CVE-2024-32002, CVE-2021-21300,\n     +    CVE-2019-1354, CVE-2019-1353, CVE-2019-1352, and CVE-2019-1349 should\n     +    have been low-severity vulnerabilities but were elevated to\n     +    critical/high severity by the attack vector that allows a weakness where\n     +    files inside `.git/` can be inadvertently written during a `git clone`\n     +    to escalate to a Remote Code Execution attack by virtue of installing a\n     +    malicious `post-checkout` hook that Git will then run at the end of the\n     +    operation without giving the user a chance to see what code is executed.\n      \n     -    In the meantime, we've added code to compute the SHA-256 checksum of a\n     -    given hook and compare that checksum against a list of known-safe ones.\n     +    Unfortunately, Git LFS uses a similar strategy to install its own\n     +    `post-checkout` hook during a `git clone`; In fact, Git LFS is\n     +    installing four separate hooks while running the `smudge` filter.\n      \n     -    Let's simplify the logic by adding to said list when copying the\n     -    templates' hooks.\n     +    While this pattern is probably in want of being improved by introducing\n     +    better support in Git for Git LFS and other tools wishing to register\n     +    hooks to be run at various stages of Git's commands, let's undo the\n     +    clone protections to unbreak Git LFS-enabled clones.\n      \n     -    We need to be careful to support multi-process operations such as\n     -    recursive submodule clones: In such a scenario, the list of SHA-256\n     -    checksums that is kept in memory is not enough, we also have to pass the\n     -    information down to child processes via `GIT_CONFIG_PARAMETERS`.\n     -\n     -    Extend the regression test in t5601 to ensure that recursive clones are\n     -    handled as expected.\n     -\n     -    Note: Technically there is no way that the checksums computed while\n     -    initializing the submodules' gitdirs can be passed to the process that\n     -    performs the checkout: For historical reasons, these operations are\n     -    performed in processes spawned in separate loops from the\n     -    super-project's `git clone` process. But since the templates from which\n     -    the submodules are initialized are the very same as the ones from which\n     -    the super-project is initialized, we can get away with using the list of\n     -    SHA-256 checksums that is computed when initializing the super-project\n     -    and passing that down to the `submodule--helper` processes that perform\n     -    the recursive checkout.\n     +    This reverts commit 8db1e8743c0 (clone: prevent hooks from running\n     +    during a clone, 2024-03-28).\n      \n          Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n      \n     - ## builtin/init-db.c ##\n     -@@\n     - #include \"exec-cmd.h\"\n     - #include \"parse-options.h\"\n     - #include \"worktree.h\"\n     -+#include \"run-command.h\"\n     -+#include \"hook.h\"\n     + ## builtin/clone.c ##\n     +@@ builtin/clone.c: int cmd_clone(int argc, const char **argv, const char *prefix)\n     + \tint err = 0, complete_refs_before_fetch = 1;\n     + \tint submodule_progress;\n     + \tint filter_submodules = 0;\n     +-\tconst char *template_dir;\n     +-\tchar *template_dir_dup = NULL;\n       \n     - #ifdef NO_TRUSTABLE_FILEMODE\n     - #define TEST_FILEMODE 0\n     -@@ builtin/init-db.c: static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,\n     - \tsize_t path_baselen = path->len;\n     - \tsize_t template_baselen = template_path->len;\n     - \tstruct dirent *de;\n     -+\tint is_hooks_dir = ends_with(template_path->buf, \"/hooks/\");\n     + \tstruct transport_ls_refs_options transport_ls_refs_options =\n     + \t\tTRANSPORT_LS_REFS_OPTIONS_INIT;\n     +@@ builtin/clone.c: int cmd_clone(int argc, const char **argv, const char *prefix)\n     + \t\tusage_msg_opt(_(\"You must specify a repository to clone.\"),\n     + \t\t\tbuiltin_clone_usage, builtin_clone_options);\n       \n     - \t/* Note: if \".git/hooks\" file exists in the repository being\n     - \t * re-initialized, /etc/core-git/templates/hooks/update would\n     -@@ builtin/init-db.c: static void copy_templates_1(struct strbuf *path, struct strbuf *template_path,\n     - \t\t\tstrbuf_release(&lnk);\n     +-\txsetenv(\"GIT_CLONE_PROTECTION_ACTIVE\", \"true\", 0 /* allow user override */);\n     +-\ttemplate_dir = get_template_dir(option_template);\n     +-\tif (*template_dir && !is_absolute_path(template_dir))\n     +-\t\ttemplate_dir = template_dir_dup =\n     +-\t\t\tabsolute_pathdup(template_dir);\n     +-\txsetenv(\"GIT_CLONE_TEMPLATE_DIR\", template_dir, 1);\n     +-\n     + \tif (option_depth || option_since || option_not.nr)\n     + \t\tdeepen = 1;\n     + \tif (option_single_branch == -1)\n     +@@ builtin/clone.c: int cmd_clone(int argc, const char **argv, const char *prefix)\n       \t\t}\n     - \t\telse if (S_ISREG(st_template.st_mode)) {\n     -+\t\t\tif (is_hooks_dir &&\n     -+\t\t\t    is_executable(template_path->buf))\n     -+\t\t\t\tadd_safe_hook(template_path->buf);\n     -+\n     - \t\t\tif (copy_file(path->buf, template_path->buf, st_template.st_mode))\n     - \t\t\t\tdie_errno(_(\"cannot copy '%s' to '%s'\"),\n     - \t\t\t\t\t  template_path->buf, path->buf);\n     + \t}\n     + \n     +-\tinit_db(git_dir, real_git_dir, template_dir, GIT_HASH_UNKNOWN, NULL,\n     ++\tinit_db(git_dir, real_git_dir, option_template, GIT_HASH_UNKNOWN, NULL,\n     + \t\tINIT_DB_QUIET);\n     + \n     + \tif (real_git_dir) {\n     +@@ builtin/clone.c: int cmd_clone(int argc, const char **argv, const char *prefix)\n     + \tfree(unborn_head);\n     + \tfree(dir);\n     + \tfree(path);\n     +-\tfree(template_dir_dup);\n     + \tUNLEAK(repo);\n     + \tjunk_mode = JUNK_LEAVE_ALL;\n     + \n      \n       ## hook.c ##\n      @@\n     + #include \"run-command.h\"\n       #include \"config.h\"\n     - #include \"strmap.h\"\n       \n      -static int identical_to_template_hook(const char *name, const char *path)\n      -{\n     @@ hook.c\n      -\treturn ret;\n      -}\n      -\n     - static struct strset safe_hook_sha256s = STRSET_INIT;\n     - static int safe_hook_sha256s_initialized;\n     - \n     -@@ hook.c: static int get_sha256_of_file_contents(const char *path, char *sha256)\n     - \treturn 0;\n     - }\n     - \n     -+void add_safe_hook(const char *path)\n     -+{\n     -+\tchar sha256[GIT_SHA256_HEXSZ + 1] = { '\\0' };\n     -+\n     -+\tif (!get_sha256_of_file_contents(path, sha256)) {\n     -+\t\tchar *p;\n     -+\n     -+\t\tstrset_add(&safe_hook_sha256s, sha256);\n     -+\n     -+\t\t/* support multi-process operations e.g. recursive clones */\n     -+\t\tp = xstrfmt(\"safe.hook.sha256=%s\", sha256);\n     -+\t\tgit_config_push_parameter(p);\n     -+\t\tfree(p);\n     -+\t}\n     -+}\n     -+\n     - static int safe_hook_cb(const char *key, const char *value, void *d)\n     + const char *find_hook(const char *name)\n       {\n     - \tstruct strset *set = d;\n     + \tstatic struct strbuf path = STRBUF_INIT;\n      @@ hook.c: const char *find_hook(const char *name)\n     + \t\t}\n       \t\treturn NULL;\n       \t}\n     - \tif (!git_hooks_path && git_env_bool(\"GIT_CLONE_PROTECTION_ACTIVE\", 0) &&\n     --\t    !identical_to_template_hook(name, path.buf) &&\n     - \t    !is_hook_safe_during_clone(name, path.buf, sha256))\n     - \t\tdie(_(\"active `%s` hook found during `git clone`:\\n\\t%s\\n\"\n     - \t\t      \"For security reasons, this is disallowed by default.\\n\"\n     -\n     - ## hook.h ##\n     -@@ hook.h: int run_hooks(const char *hook_name);\n     -  * hook. This function behaves like the old run_hook_le() API.\n     -  */\n     - int run_hooks_l(const char *hook_name, ...);\n     -+\n     -+/**\n     -+ * Mark the contents of the provided path as safe to run during a clone\n     -+ * operation.\n     -+ *\n     -+ * This function is mainly used when copying templates to mark the\n     -+ * just-copied hooks as benign.\n     -+ */\n     -+void add_safe_hook(const char *path);\n     -+\n     - #endif\n     -\n     - ## setup.c ##\n     -@@\n     - #include \"promisor-remote.h\"\n     - #include \"quote.h\"\n     - #include \"exec-cmd.h\"\n     -+#include \"hook.h\"\n     +-\tif (!git_hooks_path && git_env_bool(\"GIT_CLONE_PROTECTION_ACTIVE\", 0) &&\n     +-\t    !identical_to_template_hook(name, path.buf))\n     +-\t\tdie(_(\"active `%s` hook found during `git clone`:\\n\\t%s\\n\"\n     +-\t\t      \"For security reasons, this is disallowed by default.\\n\"\n     +-\t\t      \"If this is intentional and the hook should actually \"\n     +-\t\t      \"be run, please\\nrun the command again with \"\n     +-\t\t      \"`GIT_CLONE_PROTECTION_ACTIVE=false`\"),\n     +-\t\t    name, path.buf);\n     + \treturn path.buf;\n     + }\n       \n     - static int inside_git_dir = -1;\n     - static int inside_work_tree = -1;\n      \n       ## t/t5601-clone.sh ##\n     -@@ t/t5601-clone.sh: test_expect_success 'clone with init.templatedir runs hooks' '\n     - \t\tgit config --unset init.templateDir &&\n     - \t\t! grep \"active .* hook found\" err &&\n     - \t\ttest_path_is_missing hook-run-local-config/hook.run\n     -+\t) &&\n     -+\n     -+\ttest_config_global protocol.file.allow always &&\n     -+\tgit -C tmpl/hooks submodule add \"$(pwd)/tmpl/hooks\" sub &&\n     -+\ttest_tick &&\n     -+\tgit -C tmpl/hooks add .gitmodules sub &&\n     -+\tgit -C tmpl/hooks commit -m submodule &&\n     -+\n     -+\t(\n     -+\t\tsane_unset GIT_TEMPLATE_DIR &&\n     -+\t\tNO_SET_GIT_TEMPLATE_DIR=t &&\n     -+\t\texport NO_SET_GIT_TEMPLATE_DIR &&\n     -+\n     -+\t\tgit -c init.templateDir=\"$(pwd)/tmpl\" \\\n     -+\t\t\tclone --recurse-submodules \\\n     -+\t\t\ttmpl/hooks hook-run-submodule 2>err &&\n     -+\t\t! grep \"active .* hook found\" err &&\n     -+\t\ttest_path_is_file hook-run-submodule/hook.run &&\n     -+\t\ttest_path_is_file hook-run-submodule/sub/hook.run\n     - \t)\n     +@@ t/t5601-clone.sh: test_expect_success 'batch missing blob request does not inadvertently try to fe\n     + \tgit clone --filter=blob:limit=0 \"file://$(pwd)/server\" client\n       '\n       \n     +-test_expect_success 'clone with init.templatedir runs hooks' '\n     +-\tgit init tmpl/hooks &&\n     +-\twrite_script tmpl/hooks/post-checkout <<-EOF &&\n     +-\techo HOOK-RUN >&2\n     +-\techo I was here >hook.run\n     +-\tEOF\n     +-\tgit -C tmpl/hooks add . &&\n     +-\ttest_tick &&\n     +-\tgit -C tmpl/hooks commit -m post-checkout &&\n     +-\n     +-\ttest_when_finished \"git config --global --unset init.templateDir || :\" &&\n     +-\ttest_when_finished \"git config --unset init.templateDir || :\" &&\n     +-\t(\n     +-\t\tsane_unset GIT_TEMPLATE_DIR &&\n     +-\t\tNO_SET_GIT_TEMPLATE_DIR=t &&\n     +-\t\texport NO_SET_GIT_TEMPLATE_DIR &&\n     +-\n     +-\t\tgit -c core.hooksPath=\"$(pwd)/tmpl/hooks\" \\\n     +-\t\t\tclone tmpl/hooks hook-run-hookspath 2>err &&\n     +-\t\t! grep \"active .* hook found\" err &&\n     +-\t\ttest_path_is_file hook-run-hookspath/hook.run &&\n     +-\n     +-\t\tgit -c init.templateDir=\"$(pwd)/tmpl\" \\\n     +-\t\t\tclone tmpl/hooks hook-run-config 2>err &&\n     +-\t\t! grep \"active .* hook found\" err &&\n     +-\t\ttest_path_is_file hook-run-config/hook.run &&\n     +-\n     +-\t\tgit clone --template=tmpl tmpl/hooks hook-run-option 2>err &&\n     +-\t\t! grep \"active .* hook found\" err &&\n     +-\t\ttest_path_is_file hook-run-option/hook.run &&\n     +-\n     +-\t\tgit config --global init.templateDir \"$(pwd)/tmpl\" &&\n     +-\t\tgit clone tmpl/hooks hook-run-global-config 2>err &&\n     +-\t\tgit config --global --unset init.templateDir &&\n     +-\t\t! grep \"active .* hook found\" err &&\n     +-\t\ttest_path_is_file hook-run-global-config/hook.run &&\n     +-\n     +-\t\t# clone ignores local `init.templateDir`; need to create\n     +-\t\t# a new repository because we deleted `.git/` in the\n     +-\t\t# `setup` test case above\n     +-\t\tgit init local-clone &&\n     +-\t\tcd local-clone &&\n     +-\n     +-\t\tgit config init.templateDir \"$(pwd)/../tmpl\" &&\n     +-\t\tgit clone ../tmpl/hooks hook-run-local-config 2>err &&\n     +-\t\tgit config --unset init.templateDir &&\n     +-\t\t! grep \"active .* hook found\" err &&\n     +-\t\ttest_path_is_missing hook-run-local-config/hook.run\n     +-\t)\n     +-'\n     +-\n     + . \"$TEST_DIRECTORY\"/lib-httpd.sh\n     + start_httpd\n     + \n 8:  4b0a636d41a = 6:  5c576e889d8 Revert \"Add a helper function to compare file contents\"\n\n-- \ngitgitgadget\n"},{"id":"495112","messageId":"961dfc35f426388d660cca4e92f43e169819886a.1716236526.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.v3.git.1716236526.gitgitgadget@gmail.com","subject":"[PATCH v3 2/6] init: use the correct path of the templates directory again","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-20T20:22:01Z","receivedAt":"2024-05-20T20:22:12Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nIn df93e407f06 (init: refactor the template directory discovery into its\nown function, 2024-03-29), I refactored the way the templates directory\nis discovered.\n\nThe refactoring was faithful, but missed a reference in the `Makefile`\nwhere the `DEFAULT_GIT_TEMPLATE_DIR` constant is defined. As a\nconsequence, Git v2.45.1 and friends will always use the hard-coded path\n`/usr/share/git-core/templates`.\n\nLet's fix that by defining the `DEFAULT_GIT_TEMPLATE_DIR` when building\n`setup.o`, where that constant is actually used.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n Makefile | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/Makefile b/Makefile\nindex 093829ae283..4b1502ba2c6 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2751,7 +2751,7 @@ exec-cmd.sp exec-cmd.s exec-cmd.o: EXTRA_CPPFLAGS = \\\n \t'-DFALLBACK_RUNTIME_PREFIX=\"$(prefix_SQ)\"'\n \n builtin/init-db.sp builtin/init-db.s builtin/init-db.o: GIT-PREFIX\n-builtin/init-db.sp builtin/init-db.s builtin/init-db.o: EXTRA_CPPFLAGS = \\\n+setup.sp setup.s setup.o: EXTRA_CPPFLAGS = \\\n \t-DDEFAULT_GIT_TEMPLATE_DIR='\"$(template_dir_SQ)\"'\n \n config.sp config.s config.o: GIT-PREFIX\n-- \ngitgitgadget\n\n"},{"id":"495113","messageId":"57db89a14977bdff01f8f82cb4d6f85cc49d4b55.1716236526.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.v3.git.1716236526.gitgitgadget@gmail.com","subject":"[PATCH v3 3/6] Revert \"core.hooksPath: add some protection while cloning\"","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-20T20:22:02Z","receivedAt":"2024-05-20T20:22:13Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nThis defense-in-depth was intended to protect the clone operation\nagainst future escalations where bugs in `git clone` would allow\nattackers to write arbitrary files in the `.git/` directory would allow\nfor Remote Code Execution attacks via maliciously-placed hooks.\n\nHowever, it turns out that the `core.hooksPath` protection has\nunintentional side effects so severe that they do not justify the\nbenefit of the protections. For example, it has been reported in\nhttps://lore.kernel.org/git/FAFA34CB-9732-4A0A-87FB-BDB272E6AEE8@alchemists.io/\nthat the following invocation, which is intended to make `git clone`\nsafer, is itself broken by that protective measure:\n\n\tgit clone --config core.hooksPath=/dev/null <url>\n\nSince it turns out that the benefit does not justify the cost, let's revert\n20f3588efc6 (core.hooksPath: add some protection while cloning,\n2024-03-30).\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n config.c        | 13 +------------\n t/t1800-hook.sh | 15 ---------------\n 2 files changed, 1 insertion(+), 27 deletions(-)\n\ndiff --git a/config.c b/config.c\nindex 85b37f2ee09..8c1c4071f0d 100644\n--- a/config.c\n+++ b/config.c\n@@ -1525,19 +1525,8 @@ static int git_default_core_config(const char *var, const char *value, void *cb)\n \tif (!strcmp(var, \"core.attributesfile\"))\n \t\treturn git_config_pathname(&git_attributes_file, var, value);\n \n-\tif (!strcmp(var, \"core.hookspath\")) {\n-\t\tif (current_config_scope() == CONFIG_SCOPE_LOCAL &&\n-\t\t    git_env_bool(\"GIT_CLONE_PROTECTION_ACTIVE\", 0))\n-\t\t\tdie(_(\"active `core.hooksPath` found in the local \"\n-\t\t\t      \"repository config:\\n\\t%s\\nFor security \"\n-\t\t\t      \"reasons, this is disallowed by default.\\nIf \"\n-\t\t\t      \"this is intentional and the hook should \"\n-\t\t\t      \"actually be run, please\\nrun the command \"\n-\t\t\t      \"again with \"\n-\t\t\t      \"`GIT_CLONE_PROTECTION_ACTIVE=false`\"),\n-\t\t\t    value);\n+\tif (!strcmp(var, \"core.hookspath\"))\n \t\treturn git_config_pathname(&git_hooks_path, var, value);\n-\t}\n \n \tif (!strcmp(var, \"core.bare\")) {\n \t\tis_bare_repository_cfg = git_config_bool(var, value);\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 7ee12e6f48a..2ef3579fa7c 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -177,19 +177,4 @@ test_expect_success 'git hook run a hook with a bad shebang' '\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'clone protections' '\n-\ttest_config core.hooksPath \"$(pwd)/my-hooks\" &&\n-\tmkdir -p my-hooks &&\n-\twrite_script my-hooks/test-hook <<-\\EOF &&\n-\techo Hook ran $1\n-\tEOF\n-\n-\tgit hook run test-hook 2>err &&\n-\tgrep \"Hook ran\" err &&\n-\ttest_must_fail env GIT_CLONE_PROTECTION_ACTIVE=true \\\n-\t\tgit hook run test-hook 2>err &&\n-\tgrep \"active .core.hooksPath\" err &&\n-\t! grep \"Hook ran\" err\n-'\n-\n test_done\n-- \ngitgitgadget\n\n"},{"id":"495114","messageId":"cd14042b065e57f132c27d09005242fc500439e4.1716236526.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.v3.git.1716236526.gitgitgadget@gmail.com","subject":"[PATCH v3 4/6] tests: verify that `clone -c core.hooksPath=/dev/null` works again","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-20T20:22:03Z","receivedAt":"2024-05-20T20:22:14Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nAs part of the protections added in Git v2.45.1 and friends,\nrepository-local `core.hooksPath` settings are no longer allowed, as a\ndefense-in-depth mechanism to prevent future Git vulnerabilities to\nraise to critical level if those vulnerabilities inadvertently allow the\nrepository-local config to be written.\n\nWhat the added protection did not anticipate is that such a\nrepository-local `core.hooksPath` can not only be used to point to\nmaliciously-placed scripts in the current worktree, but also to\n_prevent_ hooks from being called altogether.\n\nWe just reverted the `core.hooksPath` protections, based on the Git\nmaintainer's recommendation in\nhttps://lore.kernel.org/git/xmqq4jaxvm8z.fsf@gitster.g/ to address this\nconcern as well as related ones. Let's make sure that we won't regress\nwhile trying to protect the clone operation further.\n\nReported-by: Brooke Kuhlmann <brooke@alchemists.io>\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n t/t1350-config-hooks-path.sh | 7 +++++++\n 1 file changed, 7 insertions(+)\n\ndiff --git a/t/t1350-config-hooks-path.sh b/t/t1350-config-hooks-path.sh\nindex f6dc83e2aab..45a04929170 100755\n--- a/t/t1350-config-hooks-path.sh\n+++ b/t/t1350-config-hooks-path.sh\n@@ -41,4 +41,11 @@ test_expect_success 'git rev-parse --git-path hooks' '\n \ttest .git/custom-hooks/abc = \"$(cat actual)\"\n '\n \n+test_expect_success 'core.hooksPath=/dev/null' '\n+\tgit clone -c core.hooksPath=/dev/null . no-templates &&\n+\tvalue=\"$(git -C no-templates config --local core.hooksPath)\" &&\n+\t# The Bash used by Git for Windows rewrites `/dev/null` to `nul`\n+\t{ test /dev/null = \"$value\" || test nul = \"$value\"; }\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"495115","messageId":"0044a35567417a552cc518576670b43f7141a02e.1716236526.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.v3.git.1716236526.gitgitgadget@gmail.com","subject":"[PATCH v3 5/6] clone: drop the protections where hooks aren't run","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-20T20:22:04Z","receivedAt":"2024-05-20T20:22:15Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nAs part of the security bug-fix releases v2.39.4, ..., v2.45.1, I\nintroduced logic to safeguard `git clone` from running hooks that were\ninstalled _during_ the clone operation.\n\nThe rationale was that Git's CVE-2024-32002, CVE-2021-21300,\nCVE-2019-1354, CVE-2019-1353, CVE-2019-1352, and CVE-2019-1349 should\nhave been low-severity vulnerabilities but were elevated to\ncritical/high severity by the attack vector that allows a weakness where\nfiles inside `.git/` can be inadvertently written during a `git clone`\nto escalate to a Remote Code Execution attack by virtue of installing a\nmalicious `post-checkout` hook that Git will then run at the end of the\noperation without giving the user a chance to see what code is executed.\n\nUnfortunately, Git LFS uses a similar strategy to install its own\n`post-checkout` hook during a `git clone`; In fact, Git LFS is\ninstalling four separate hooks while running the `smudge` filter.\n\nWhile this pattern is probably in want of being improved by introducing\nbetter support in Git for Git LFS and other tools wishing to register\nhooks to be run at various stages of Git's commands, let's undo the\nclone protections to unbreak Git LFS-enabled clones.\n\nThis reverts commit 8db1e8743c0 (clone: prevent hooks from running\nduring a clone, 2024-03-28).\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n builtin/clone.c  | 12 +-----------\n hook.c           | 34 --------------------------------\n t/t5601-clone.sh | 51 ------------------------------------------------\n 3 files changed, 1 insertion(+), 96 deletions(-)\n\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex e7721f5c22c..9ec500d427e 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -937,8 +937,6 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \tint err = 0, complete_refs_before_fetch = 1;\n \tint submodule_progress;\n \tint filter_submodules = 0;\n-\tconst char *template_dir;\n-\tchar *template_dir_dup = NULL;\n \n \tstruct transport_ls_refs_options transport_ls_refs_options =\n \t\tTRANSPORT_LS_REFS_OPTIONS_INIT;\n@@ -958,13 +956,6 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \t\tusage_msg_opt(_(\"You must specify a repository to clone.\"),\n \t\t\tbuiltin_clone_usage, builtin_clone_options);\n \n-\txsetenv(\"GIT_CLONE_PROTECTION_ACTIVE\", \"true\", 0 /* allow user override */);\n-\ttemplate_dir = get_template_dir(option_template);\n-\tif (*template_dir && !is_absolute_path(template_dir))\n-\t\ttemplate_dir = template_dir_dup =\n-\t\t\tabsolute_pathdup(template_dir);\n-\txsetenv(\"GIT_CLONE_TEMPLATE_DIR\", template_dir, 1);\n-\n \tif (option_depth || option_since || option_not.nr)\n \t\tdeepen = 1;\n \tif (option_single_branch == -1)\n@@ -1112,7 +1103,7 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \t\t}\n \t}\n \n-\tinit_db(git_dir, real_git_dir, template_dir, GIT_HASH_UNKNOWN, NULL,\n+\tinit_db(git_dir, real_git_dir, option_template, GIT_HASH_UNKNOWN, NULL,\n \t\tINIT_DB_QUIET);\n \n \tif (real_git_dir) {\n@@ -1430,7 +1421,6 @@ int cmd_clone(int argc, const char **argv, const char *prefix)\n \tfree(unborn_head);\n \tfree(dir);\n \tfree(path);\n-\tfree(template_dir_dup);\n \tUNLEAK(repo);\n \tjunk_mode = JUNK_LEAVE_ALL;\n \ndiff --git a/hook.c b/hook.c\nindex fc974cee1d8..22b274b60b1 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -3,32 +3,6 @@\n #include \"run-command.h\"\n #include \"config.h\"\n \n-static int identical_to_template_hook(const char *name, const char *path)\n-{\n-\tconst char *env = getenv(\"GIT_CLONE_TEMPLATE_DIR\");\n-\tconst char *template_dir = get_template_dir(env && *env ? env : NULL);\n-\tstruct strbuf template_path = STRBUF_INIT;\n-\tint found_template_hook, ret;\n-\n-\tstrbuf_addf(&template_path, \"%s/hooks/%s\", template_dir, name);\n-\tfound_template_hook = access(template_path.buf, X_OK) >= 0;\n-#ifdef STRIP_EXTENSION\n-\tif (!found_template_hook) {\n-\t\tstrbuf_addstr(&template_path, STRIP_EXTENSION);\n-\t\tfound_template_hook = access(template_path.buf, X_OK) >= 0;\n-\t}\n-#endif\n-\tif (!found_template_hook) {\n-\t\tstrbuf_release(&template_path);\n-\t\treturn 0;\n-\t}\n-\n-\tret = do_files_match(template_path.buf, path);\n-\n-\tstrbuf_release(&template_path);\n-\treturn ret;\n-}\n-\n const char *find_hook(const char *name)\n {\n \tstatic struct strbuf path = STRBUF_INIT;\n@@ -64,14 +38,6 @@ const char *find_hook(const char *name)\n \t\t}\n \t\treturn NULL;\n \t}\n-\tif (!git_hooks_path && git_env_bool(\"GIT_CLONE_PROTECTION_ACTIVE\", 0) &&\n-\t    !identical_to_template_hook(name, path.buf))\n-\t\tdie(_(\"active `%s` hook found during `git clone`:\\n\\t%s\\n\"\n-\t\t      \"For security reasons, this is disallowed by default.\\n\"\n-\t\t      \"If this is intentional and the hook should actually \"\n-\t\t      \"be run, please\\nrun the command again with \"\n-\t\t      \"`GIT_CLONE_PROTECTION_ACTIVE=false`\"),\n-\t\t    name, path.buf);\n \treturn path.buf;\n }\n \ndiff --git a/t/t5601-clone.sh b/t/t5601-clone.sh\nindex 20deca0231b..fd029843307 100755\n--- a/t/t5601-clone.sh\n+++ b/t/t5601-clone.sh\n@@ -771,57 +771,6 @@ test_expect_success 'batch missing blob request does not inadvertently try to fe\n \tgit clone --filter=blob:limit=0 \"file://$(pwd)/server\" client\n '\n \n-test_expect_success 'clone with init.templatedir runs hooks' '\n-\tgit init tmpl/hooks &&\n-\twrite_script tmpl/hooks/post-checkout <<-EOF &&\n-\techo HOOK-RUN >&2\n-\techo I was here >hook.run\n-\tEOF\n-\tgit -C tmpl/hooks add . &&\n-\ttest_tick &&\n-\tgit -C tmpl/hooks commit -m post-checkout &&\n-\n-\ttest_when_finished \"git config --global --unset init.templateDir || :\" &&\n-\ttest_when_finished \"git config --unset init.templateDir || :\" &&\n-\t(\n-\t\tsane_unset GIT_TEMPLATE_DIR &&\n-\t\tNO_SET_GIT_TEMPLATE_DIR=t &&\n-\t\texport NO_SET_GIT_TEMPLATE_DIR &&\n-\n-\t\tgit -c core.hooksPath=\"$(pwd)/tmpl/hooks\" \\\n-\t\t\tclone tmpl/hooks hook-run-hookspath 2>err &&\n-\t\t! grep \"active .* hook found\" err &&\n-\t\ttest_path_is_file hook-run-hookspath/hook.run &&\n-\n-\t\tgit -c init.templateDir=\"$(pwd)/tmpl\" \\\n-\t\t\tclone tmpl/hooks hook-run-config 2>err &&\n-\t\t! grep \"active .* hook found\" err &&\n-\t\ttest_path_is_file hook-run-config/hook.run &&\n-\n-\t\tgit clone --template=tmpl tmpl/hooks hook-run-option 2>err &&\n-\t\t! grep \"active .* hook found\" err &&\n-\t\ttest_path_is_file hook-run-option/hook.run &&\n-\n-\t\tgit config --global init.templateDir \"$(pwd)/tmpl\" &&\n-\t\tgit clone tmpl/hooks hook-run-global-config 2>err &&\n-\t\tgit config --global --unset init.templateDir &&\n-\t\t! grep \"active .* hook found\" err &&\n-\t\ttest_path_is_file hook-run-global-config/hook.run &&\n-\n-\t\t# clone ignores local `init.templateDir`; need to create\n-\t\t# a new repository because we deleted `.git/` in the\n-\t\t# `setup` test case above\n-\t\tgit init local-clone &&\n-\t\tcd local-clone &&\n-\n-\t\tgit config init.templateDir \"$(pwd)/../tmpl\" &&\n-\t\tgit clone ../tmpl/hooks hook-run-local-config 2>err &&\n-\t\tgit config --unset init.templateDir &&\n-\t\t! grep \"active .* hook found\" err &&\n-\t\ttest_path_is_missing hook-run-local-config/hook.run\n-\t)\n-'\n-\n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n start_httpd\n \n-- \ngitgitgadget\n\n"},{"id":"495116","messageId":"5c576e889d85b34fc717b693ba526cc46836f66a.1716236527.git.gitgitgadget@gmail.com","threadId":"61509","inReplyTo":"pull.1732.v3.git.1716236526.gitgitgadget@gmail.com","subject":"[PATCH v3 6/6] Revert \"Add a helper function to compare file contents\"","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2024-05-20T20:22:05Z","receivedAt":"2024-05-20T20:22:16Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nNow that during a `git clone`, the hooks' contents are no longer\ncompared to the templates' files', the caller for which the\n`do_files_match()` function was introduced is gone, and therefore this\nfunction can be retired, too.\n\nThis reverts commit 584de0b4c23 (Add a helper function to compare file\ncontents, 2024-03-30).\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n cache.h                    | 14 ---------\n copy.c                     | 58 --------------------------------------\n t/helper/test-path-utils.c | 10 -------\n t/t0060-path-utils.sh      | 41 ---------------------------\n 4 files changed, 123 deletions(-)\n\ndiff --git a/cache.h b/cache.h\nindex 16b34799bfd..8c5fb1e1ba1 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1785,20 +1785,6 @@ int copy_fd(int ifd, int ofd);\n int copy_file(const char *dst, const char *src, int mode);\n int copy_file_with_time(const char *dst, const char *src, int mode);\n \n-/*\n- * Compare the file mode and contents of two given files.\n- *\n- * If both files are actually symbolic links, the function returns 1 if the link\n- * targets are identical or 0 if they are not.\n- *\n- * If any of the two files cannot be accessed or in case of read failures, this\n- * function returns 0.\n- *\n- * If the file modes and contents are identical, the function returns 1,\n- * otherwise it returns 0.\n- */\n-int do_files_match(const char *path1, const char *path2);\n-\n void write_or_die(int fd, const void *buf, size_t count);\n void fsync_or_die(int fd, const char *);\n int fsync_component(enum fsync_component component, int fd);\ndiff --git a/copy.c b/copy.c\nindex 8492f6fc831..4de6a110f09 100644\n--- a/copy.c\n+++ b/copy.c\n@@ -65,61 +65,3 @@ int copy_file_with_time(const char *dst, const char *src, int mode)\n \t\treturn copy_times(dst, src);\n \treturn status;\n }\n-\n-static int do_symlinks_match(const char *path1, const char *path2)\n-{\n-\tstruct strbuf buf1 = STRBUF_INIT, buf2 = STRBUF_INIT;\n-\tint ret = 0;\n-\n-\tif (!strbuf_readlink(&buf1, path1, 0) &&\n-\t    !strbuf_readlink(&buf2, path2, 0))\n-\t\tret = !strcmp(buf1.buf, buf2.buf);\n-\n-\tstrbuf_release(&buf1);\n-\tstrbuf_release(&buf2);\n-\treturn ret;\n-}\n-\n-int do_files_match(const char *path1, const char *path2)\n-{\n-\tstruct stat st1, st2;\n-\tint fd1 = -1, fd2 = -1, ret = 1;\n-\tchar buf1[8192], buf2[8192];\n-\n-\tif ((fd1 = open_nofollow(path1, O_RDONLY)) < 0 ||\n-\t    fstat(fd1, &st1) || !S_ISREG(st1.st_mode)) {\n-\t\tif (fd1 < 0 && errno == ELOOP)\n-\t\t\t/* maybe this is a symbolic link? */\n-\t\t\treturn do_symlinks_match(path1, path2);\n-\t\tret = 0;\n-\t} else if ((fd2 = open_nofollow(path2, O_RDONLY)) < 0 ||\n-\t\t   fstat(fd2, &st2) || !S_ISREG(st2.st_mode)) {\n-\t\tret = 0;\n-\t}\n-\n-\tif (ret)\n-\t\t/* to match, neither must be executable, or both */\n-\t\tret = !(st1.st_mode & 0111) == !(st2.st_mode & 0111);\n-\n-\tif (ret)\n-\t\tret = st1.st_size == st2.st_size;\n-\n-\twhile (ret) {\n-\t\tssize_t len1 = read_in_full(fd1, buf1, sizeof(buf1));\n-\t\tssize_t len2 = read_in_full(fd2, buf2, sizeof(buf2));\n-\n-\t\tif (len1 < 0 || len2 < 0 || len1 != len2)\n-\t\t\tret = 0; /* read error or different file size */\n-\t\telse if (!len1) /* len2 is also 0; hit EOF on both */\n-\t\t\tbreak; /* ret is still true */\n-\t\telse\n-\t\t\tret = !memcmp(buf1, buf2, len1);\n-\t}\n-\n-\tif (fd1 >= 0)\n-\t\tclose(fd1);\n-\tif (fd2 >= 0)\n-\t\tclose(fd2);\n-\n-\treturn ret;\n-}\ndiff --git a/t/helper/test-path-utils.c b/t/helper/test-path-utils.c\nindex 0e0de218076..f69709d674f 100644\n--- a/t/helper/test-path-utils.c\n+++ b/t/helper/test-path-utils.c\n@@ -495,16 +495,6 @@ int cmd__path_utils(int argc, const char **argv)\n \t\treturn !!res;\n \t}\n \n-\tif (argc == 4 && !strcmp(argv[1], \"do_files_match\")) {\n-\t\tint ret = do_files_match(argv[2], argv[3]);\n-\n-\t\tif (ret)\n-\t\t\tprintf(\"equal\\n\");\n-\t\telse\n-\t\t\tprintf(\"different\\n\");\n-\t\treturn !ret;\n-\t}\n-\n \tfprintf(stderr, \"%s: unknown function name: %s\\n\", argv[0],\n \t\targv[1] ? argv[1] : \"(there was none)\");\n \treturn 1;\ndiff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh\nindex 73d0e1a7f10..68e29c904a6 100755\n--- a/t/t0060-path-utils.sh\n+++ b/t/t0060-path-utils.sh\n@@ -560,45 +560,4 @@ test_expect_success !VALGRIND,RUNTIME_PREFIX,CAN_EXEC_IN_PWD '%(prefix)/ works'\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'do_files_match()' '\n-\ttest_seq 0 10 >0-10.txt &&\n-\ttest_seq -1 10 >-1-10.txt &&\n-\ttest_seq 1 10 >1-10.txt &&\n-\ttest_seq 1 9 >1-9.txt &&\n-\ttest_seq 0 8 >0-8.txt &&\n-\n-\ttest-tool path-utils do_files_match 0-10.txt 0-10.txt >out &&\n-\n-\tassert_fails() {\n-\t\ttest_must_fail \\\n-\t\ttest-tool path-utils do_files_match \"$1\" \"$2\" >out &&\n-\t\tgrep different out\n-\t} &&\n-\n-\tassert_fails 0-8.txt 1-9.txt &&\n-\tassert_fails -1-10.txt 0-10.txt &&\n-\tassert_fails 1-10.txt 1-9.txt &&\n-\tassert_fails 1-10.txt .git &&\n-\tassert_fails does-not-exist 1-10.txt &&\n-\n-\tif test_have_prereq FILEMODE\n-\tthen\n-\t\tcp 0-10.txt 0-10.x &&\n-\t\tchmod a+x 0-10.x &&\n-\t\tassert_fails 0-10.txt 0-10.x\n-\tfi &&\n-\n-\tif test_have_prereq SYMLINKS\n-\tthen\n-\t\tln -sf 0-10.txt symlink &&\n-\t\tln -s 0-10.txt another-symlink &&\n-\t\tln -s over-the-ocean yet-another-symlink &&\n-\t\tln -s \"$PWD/0-10.txt\" absolute-symlink &&\n-\t\tassert_fails 0-10.txt symlink &&\n-\t\ttest-tool path-utils do_files_match symlink another-symlink &&\n-\t\tassert_fails symlink yet-another-symlink &&\n-\t\tassert_fails symlink absolute-symlink\n-\tfi\n-'\n-\n test_done\n-- \ngitgitgadget\n"},{"id":"495119","messageId":"4f7cb327-2ccd-deee-8727-ae0302e36e45@gmx.de","threadId":"61509","inReplyTo":"736b43a1-a371-4d6f-7739-b5b64e060585@gmx.de","subject":"Re: [PATCH v2 5/8] hook(clone protections): add escape hatch","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2024-05-20T21:03:59Z","receivedAt":"2024-05-20T21:04:17Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi,\n\nOn Mon, 20 May 2024, Johannes Schindelin wrote:\n\n> Let me quickly iterate on this here patch series (as well as the\n> `tentative/maint-*` branches) so that we can accelerate toward a fixed\n> version again\n\nv3 is on the list. The `tentative/maint-*` branches have been pushed to\nhttps://github.com/dscho/git, with these commit OIDs:\n\nb9a96c4e5dc4e04258214ab772972a0e1eefd3c5 refs/heads/tentative/maint-2.39\n4bf5d57da62f91db9b74d490d5dae69e65cbdc73 refs/heads/tentative/maint-2.40\n5215e4e36879d1ee0ad5da7790f4598c3314ed45 refs/heads/tentative/maint-2.41\n33efa2ad1a6c14fc5d8bc5cdf38ba13b25926b42 refs/heads/tentative/maint-2.42\n0aeca2f80b17fcfdd9186c585ce84004ed43f46a refs/heads/tentative/maint-2.43\n9953011fcdd895fd3ff4e2f2e5ff266eaf8b0b49 refs/heads/tentative/maint-2.44\naeddcb02756259e4b221f37a60e4ee1ece3889f1 refs/heads/tentative/maint-2.45\n\nCiao,\nJohannes\n"},{"id":"495136","messageId":"xmqqseycca0z.fsf@gitster.g","threadId":"61509","inReplyTo":"pull.1732.v3.git.1716236526.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 0/6] Various fixes for v2.45.1 and friends","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-20T23:56:44Z","receivedAt":"2024-05-20T23:56:46Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Johannes Schindelin via GitGitGadget\" <gitgitgadget@gmail.com>\nwrites:\n\n> This patch series is based on maint-2.39 to allow for (relatively) easy\n> follow-up versions v2.39.5, ..., v2.45.2.\n>\n> Changes since v2:\n>\n>  * instead of introducing an escape hatch for the clone protections and\n>    special-casing Git LFS, drop the clone protections\n\nIt is debatable if we are ripping out clone \"protection\" or a new\nrestriction on executing hooks before the end of clone that has\nbackfired. \n\nIn any case, I just compared the result of applying these patches to\nv2.39.4 with the result of reverting the following out of v2.39.4:\n\n    584de0b4 (Add a helper function to compare file contents, 2024-03-30)\n    8db1e874 (clone: prevent hooks from running during a clone, 2024-03-28)\n    20f3588e (core.hooksPath: add some protection while cloning, 2024-03-30)\n\nand the differences was exactly as I expected.  A Makefile fix and a\nnew test added to t1350 are the extra in the series, but otherwise\nthe patches are essentially reversion of these three steps.  Very\nnicely done.\n\nThanks for a quick turnaround.  Will take further look.\n\n\n"},{"id":"495139","messageId":"xmqqikz7d90g.fsf@gitster.g","threadId":"61509","inReplyTo":"xmqqseycca0z.fsf@gitster.g","subject":"Re: [PATCH v3 0/6] Various fixes for v2.45.1 and friends","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-21T05:33:19Z","receivedAt":"2024-05-21T05:33:24Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> In any case, I just compared the result of applying these patches to\n> v2.39.4 with the result of reverting the following out of v2.39.4:\n>\n>     584de0b4 (Add a helper function to compare file contents, 2024-03-30)\n>     8db1e874 (clone: prevent hooks from running during a clone, 2024-03-28)\n>     20f3588e (core.hooksPath: add some protection while cloning, 2024-03-30)\n>\n> and the differences was exactly as I expected.  A Makefile fix and a\n> new test added to t1350 are the extra in the series, but otherwise\n> the patches are essentially reversion of these three steps.  Very\n> nicely done.\n>\n> Thanks for a quick turnaround.  Will take further look.\n\nI completed merge-up exercise and compared the result with your\n\"tentative\" cascade from maint-2.39 to maint-2.45 tracks.  \n\nThe differences came from pointed cherry-picks (like 'ci: avoid bare\n\"gcc\" for osx-gcc job') looked minimal and sensible.  I wonder what\nthe best way to do a public review of this kind of history, though.\n\n$ git log --oneline --graph maint-2.45..dscho/tentative/maint-2.45\n* aeddcb0275 Git 2.45.2\n* 65f0d62523 Sync with 2.44.2\n* 9953011fcd Git 2.44.2\n* f78818b645 Sync with 2.43.5\n* 0aeca2f80b Git 2.43.5\n* 0cc3782b1a Sync with 2.42.3\n* 33efa2ad1a Git 2.42.3\n* 30195eb2b6 Sync with 2.41.2\n* 5215e4e368 Git 2.41.2\n* 9d6788fd73 Sync with 2.40.3\n* 4bf5d57da6 Git 2.40.3\n* 9f7a956be5 Sync with 2.39.5\n* b9a96c4e5d Git 2.39.5\n\nAll of the above (and the one below) are merging up, resolving\nconflicts, and updating release notes and GIT-VERSION-GEN.\n\n*   b674c6f66c Merge branch 'js/fix-v2.39.4-regressions' into maint-2.39\n|\\  \n| * 5c576e889d Revert \"Add a helper function to compare file contents\"\n| * 0044a35567 clone: drop the protections where hooks aren't run\n| * cd14042b06 tests: verify that `clone -c core.hooksPath=/dev/null` works again\n| * 57db89a149 Revert \"core.hooksPath: add some protection while cloning\"\n| * 961dfc35f4 init: use the correct path of the templates directory again\n| * d4a003bf2c hook: plug a new memory leak\n\nThe above 6 patches all appeared on the list in this \"v3\" thread.\n\n* 883ca51e0a Merge branch 'jk/ci-macos-gcc13-fix' into 'maint-2.39'\n\nThis is a merge of the following three patches to maint-2.39\n\n* d4543be3f2 ci: stop installing \"gcc-13\" for osx-gcc\n* 2aef8020d2 ci: avoid bare \"gcc\" for osx-gcc job\n* f3e5bdfebc ci: drop mention of BREW_INSTALL_PACKAGES variable\n\nThese three patches were taken from jk/ci-macos-gcc13-fix that was\nforked from v2.45.0 and rebased them on top of v2.39.4.  The bottom\none seems to have been adjusted for the older contexts, which during\nsubsequent merging-up has been adjusted back again for the more\nrecent contexts (e.g., we used to use $HOME/bin but use $P4_PATH for\nPerforce these days, and such differences in the base version appear\nin the context for \"BREW_INSTALL_PACKAGES\" change).\n\nSo, in short, I didn't see anything unexpected to see in these\nbranches.  The \"ci\" fixes were already reviewed elsewhere (even\nthough there are slight deviations), so if people are OK with the 6\npatches in this thread, I would say we are good to go.\n\nThanks.\n\n\n\n\n\n"},{"id":"495182","messageId":"xmqqle439gno.fsf@gitster.g","threadId":"61509","inReplyTo":"xmqqikz7d90g.fsf@gitster.g","subject":"Re: [PATCH v3 0/6] Various fixes for v2.45.1 and friends","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-21T18:14:03Z","receivedAt":"2024-05-21T18:14:07Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> So, in short, I didn't see anything unexpected to see in these\n> branches.  The \"ci\" fixes were already reviewed elsewhere (even\n> though there are slight deviations), so if people are OK with the 6\n> patches in this thread, I would say we are good to go.\n\nMaybe I spoke a bit too early, and we may need to redo (not\nnecessarily revert) f4aa8c8b as well.\n\n\n"},{"id":"495246","messageId":"Zk0hKCsbjFGkguFQ@tapette.crustytoothpaste.net","threadId":"61509","inReplyTo":"pull.1732.v3.git.1716236526.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 0/6] Various fixes for v2.45.1 and friends","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2024-05-21T22:33:12Z","receivedAt":"2024-05-21T22:33:20Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2024-05-20 at 20:21:59, Johannes Schindelin via GitGitGadget wrote:\n> There have been a couple of issues that were reported about v2.45.1, and in\n> addition I have noticed some myself:\n> \n>  * a memory leak in the clone protection logic\n>  * a missed adjustment in the Makefile that leads to an incorrect templates\n>    path in v2.39.4, v2.40.2 and v2.41.1 (but not in v2.42.2, ..., v2.45.1)\n>  * an overzealous core.hooksPath check\n>  * that Git LFS clone problem where it exits with an error (even if the\n>    clone often succeeded...)\n> \n> This patch series is based on maint-2.39 to allow for (relatively) easy\n> follow-up versions v2.39.5, ..., v2.45.2.\n\nI looked at this series and seems fine.  I tested it with the latest\nHEAD of Git LFS and it seems to function as expected.  I appreciate the\nprompt fixes.\n\n(My apologies for not getting back to this sooner.  I took a long\nweekend for the Victoria Day holiday and was busy riding rollercoasters\ninstead of reading the list.)\n-- \nbrian m. carlson (they/them or he/him)\nToronto, Ontario, CA\n"},{"id":"495247","messageId":"xmqq4jaqkcul.fsf@gitster.g","threadId":"61509","inReplyTo":"Zk0hKCsbjFGkguFQ@tapette.crustytoothpaste.net","subject":"Re: [PATCH v3 0/6] Various fixes for v2.45.1 and friends","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-21T22:40:50Z","receivedAt":"2024-05-21T22:40:53Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> (My apologies for not getting back to this sooner.  I took a long\n> weekend for the Victoria Day holiday and was busy riding rollercoasters\n> instead of reading the list.)\n\nThat's fine.  We all must have fun sometimes ;-)\n"},{"id":"495249","messageId":"xmqqv836ix6c.fsf@gitster.g","threadId":"61509","inReplyTo":"Zk0hKCsbjFGkguFQ@tapette.crustytoothpaste.net","subject":"Re: [PATCH v3 0/6] Various fixes for v2.45.1 and friends","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2024-05-21T23:04:43Z","receivedAt":"2024-05-21T23:04:49Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"brian m. carlson\" <sandals@crustytoothpaste.net> writes:\n\n> I looked at this series and seems fine.  I tested it with the latest\n> HEAD of Git LFS and it seems to function as expected.  I appreciate the\n> prompt fixes.\n\nI've merged the 11 patches (the 12-patch series without the last\nstep) https://lore.kernel.org/git/xmqqed9u95l0.fsf@gitster.g/ to\n'seen'.  These 11 patches contains Dscho's 6 patches but also\ncontains backports Dscho had on his \"tentative\" topic branch, plus\ntwo additional backports to help tests pass.  Additionally what is\nin 'seen' obviously does not update GIT-VERSION-GEN or add new\nrelease notes files.  Other than that, it is the same topic, only\ndone in a bit more open way to make it easier to apply further\npolish.  Testing is appreciated.\n\nHopefully they can be merged down to 'next' and then 'master'\nsoonish, but will be updated if a new issue is discovered.  After\nthen, we'll do a release engineering to merge them to all the\nmaintenance tracks affected by the recent security updates and we\ncan go from there.  Hopefully, as not critical vulnerability fix but\na \"layered defence\" on top, we probably do not have to merge down\nwhatever additional follow-up changes beyond the latest maintenance\ntrack (i.e. maint-2.45).\n\nThanks.\n"}]}