{"thread":{"id":"66444","subject":"[RFC PATCH 0/4] sign a SHA-256 digest of the tree in commits and tags","startedAt":"2026-10-02T08:18:51Z","lastAt":"2026-10-06T09:00:45Z","messageCount":14,"participants":["Scott Chacon","Junio C Hamano","brian m. carlson","Patrick Steinhardt","Christian Couder"],"isPatch":true,"patchVersion":1,"patchTotal":4},"messages":[{"id":"553914","messageId":"20261002081846.25144-1-scott@gitbutler.net","threadId":"66444","inReplyTo":null,"subject":"[RFC PATCH 0/4] sign a SHA-256 digest of the tree in commits and tags","fromName":"Scott Chacon","fromEmail":"scott@gitbutler.net","sentAt":"2026-10-02T08:18:42Z","receivedAt":"2026-10-02T08:18:51Z","isPatch":true,"body":"I'm concerned about the ecosystem impact of moving the `git init` default\nhashing function to SHA-256 in 3.0. I have suggested that it may be more \nfeasible with similar benefits to add the ability to inject an independently\ncalculated and verifiable tree content sha into signed objects instead.\n\nThis RFC series is meant to demonstrate how this might work.\n\nIt adds the ability to directly rehash the full tree contents when signing a\ncommit or tag with SHA-256 without the repository needing to be in the sha256\nobject format.\n\nIn this series \"git tag -s --hash=sha256\" and \"git commit -S --hash=sha256\" \ncompute a SHA-256 digest over every file in the tree (and submodules) and put\nthat additional hash in a header before signing:\n\n  object 78bd45828aa36fbde3161f49da15272dff3d06f5\n  type commit\n  tag v1.0\n  tagger A U Thor <author@example.com> 1790749714 +0200\n  tree-sha256 775aff90d07c9a73f19ef83ab89bd8e95d1d835325d53cc57a2232b015783d89\n\n  Release 1.0\n  -----BEGIN SSH SIGNATURE-----\n\nFor a commit it goes after \"committer\", before \"gpgsig\". Setting\ngpg.treeHash=sha256 makes it the default for everything you sign.\n\nThe digest is SHA-256 over one record per file, sorted by path:\n\n  <hex sha256 of content> SP <path> NUL\n\nThe file mode isn't included. Submodules are followed into their own\nrepositories and contribute \"<hex digest of their tree> SP <path>/ NUL\",\nso the signature covers their contents too; if a submodule isn't\navailable, we fail rather than sign something we can't vouch for.\n\nOld versions of Git are fine with the new header: fsck ignores extra\nheaders after \"tagger\" by default (and always for commits), and \"git\ntag -v\" and \"git verify-commit\" check the signature as before.\n\n  - Patch 1 adds the digest, with a test-tool helper so it can be\n    tested on its own.\n  - Patches 2 and 3 add --hash to \"git tag\" and \"git commit\".\n  - Patch 4 adds gpg.treeHash.\n\nFrom a speed perspective, it's not fast but it's not slow. The default\nbuild on my M5 is 245ms for a git.git signed tag call, ~5s for the Linux\ntree. An accelerated OpenSSL build is 135ms for git.git, 1.8s for Linux.\n\nHowever, this is single threaded. We could easily do parallel hashing which\nshould make it many times faster - my previous tests in Rust on 18 threads\non my M5 did git.git in 36ms and Linux tree in 0.5s (verified the same hash).\n\nNot in this series, and what I'd like opinions on:\n\n  - Any interest? Would the list find this approach a viable alternative\n    to not switching the default hash function to sha-256 in 3.0? Not that\n    it wouldn't be an available object format, but that it wouldn't need to\n    be the default one.\n\n  - Verification. \"git tag -v\" and \"git verify-commit\" don't recompute\n    the digest yet. I'd like to agree on the format before adding that.\n\n  - Excluding submodules. Large projects can have submodules that most\n    people never check out, and they can't sign with --hash today. One\n    option is an \"excluded:<commit>\" header that still covers the\n    pinned commit but not its contents, with a header listing the\n    excluded paths so that verification can report them.\n\n  - Naming. The header is \"tree-sha256\", the option \"--hash\", and the\n    config \"gpg.treeHash\". I'm not attached to any of them.\n\nScott Chacon (4):\n  tree-sha256: hash the contents of a tree with SHA-256\n  tag: add --hash=sha256 to sign a tree-sha256 header\n  commit: add --hash=sha256 to sign a tree-sha256 header\n  gpg: add gpg.treeHash to sign a tree-sha256 header by default\n\n Documentation/config/gpg.adoc |   6 +\n Documentation/git-commit.adoc |  11 +-\n Documentation/git-tag.adoc    |  11 +-\n Makefile                      |   2 +\n builtin/commit.c              |  46 ++++++-\n builtin/tag.c                 |  41 +++++-\n meson.build                   |   1 +\n t/helper/meson.build          |   1 +\n t/helper/test-tool.c          |   1 +\n t/helper/test-tool.h          |   1 +\n t/helper/test-tree-sha256.c   |  31 +++++\n t/meson.build                 |   2 +\n t/t1018-tree-sha256.sh        | 123 +++++++++++++++++\n t/t7032-tree-sha256-signed.sh | 169 +++++++++++++++++++++++\n tree-sha256.c                 | 247 ++++++++++++++++++++++++++++++++++\n tree-sha256.h                 |  36 +++++\n 16 files changed, 720 insertions(+), 9 deletions(-)\n create mode 100644 t/helper/test-tree-sha256.c\n create mode 100755 t/t1018-tree-sha256.sh\n create mode 100755 t/t7032-tree-sha256-signed.sh\n create mode 100644 tree-sha256.c\n create mode 100644 tree-sha256.h\n\n\nbase-commit: a018953688f1b10bddf91bff8747068f5f4746a4\n-- \n2.50.1 (Apple Git-155)\n\n"},{"id":"553915","messageId":"20261002081846.25144-2-scott@gitbutler.net","threadId":"66444","inReplyTo":"20261002081846.25144-1-scott@gitbutler.net","subject":"[RFC PATCH 1/4] tree-sha256: hash the contents of a tree with SHA-256","fromName":"Scott Chacon","fromEmail":"scott@gitbutler.net","sentAt":"2026-10-02T08:18:43Z","receivedAt":"2026-10-02T08:18:52Z","isPatch":true,"body":"Add a way to compute a SHA-256 digest of the contents of a tree that\ndoesn't depend on the object format, so that it can be put in the\nsigned payload. Each blob in the tree, recursively, becomes one record, \nand the digest is SHA-256 over the records sorted by path:\n\n  <hex sha256 of content> SP <path> NUL\n\nA gitlink is followed into the submodule's own repository, and the\ntree of the commit it pins is hashed in the same way, giving one record\nwith a trailing slash on the path:\n\n  <hex digest of submodule tree> SP <path>/ NUL\n\nA path never ends in a slash in a tree, so these can't be confused\nwith files. If a submodule isn't available, or doesn't have the pinned\ncommit, we can't say anything about its contents and so fail, listing\nevery one of them. A submodule that pins a commit already being hashed\nabove it would be recorded as \"cycle:<commit>\" instead, which can't\nhappen without a hash collision but keeps the walk from going around\nforever if it does.\n\nThe digest can be reproduced with \"git ls-tree -r\", \"git cat-file\"\nand sha256sum, which is how the new test checks it, through a new\n\"test-tool tree-sha256\". The following patches use it in \"git tag\"\nand \"git commit\".\n\n---\n Makefile                    |   2 +\n meson.build                 |   1 +\n t/helper/meson.build        |   1 +\n t/helper/test-tool.c        |   1 +\n t/helper/test-tool.h        |   1 +\n t/helper/test-tree-sha256.c |  31 +++++\n t/meson.build               |   1 +\n t/t1018-tree-sha256.sh      | 123 +++++++++++++++++++\n tree-sha256.c               | 238 ++++++++++++++++++++++++++++++++++++\n tree-sha256.h               |  30 +++++\n 10 files changed, 429 insertions(+)\n create mode 100644 t/helper/test-tree-sha256.c\n create mode 100755 t/t1018-tree-sha256.sh\n create mode 100644 tree-sha256.c\n create mode 100644 tree-sha256.h\n\ndiff --git a/Makefile b/Makefile\nindex c649c93c51..e042163635 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -878,6 +878,7 @@ TEST_BUILTINS_OBJS += test-submodule.o\n TEST_BUILTINS_OBJS += test-subprocess.o\n TEST_BUILTINS_OBJS += test-synthesize.o\n TEST_BUILTINS_OBJS += test-trace2.o\n+TEST_BUILTINS_OBJS += test-tree-sha256.o\n TEST_BUILTINS_OBJS += test-truncate.o\n TEST_BUILTINS_OBJS += test-userdiff.o\n TEST_BUILTINS_OBJS += test-wildmatch.o\n@@ -1357,6 +1358,7 @@ LIB_OBJS += trailer.o\n LIB_OBJS += transport-helper.o\n LIB_OBJS += transport.o\n LIB_OBJS += tree-diff.o\n+LIB_OBJS += tree-sha256.o\n LIB_OBJS += tree-walk.o\n LIB_OBJS += tree.o\n LIB_OBJS += unpack-trees.o\ndiff --git a/meson.build b/meson.build\nindex 0a95d90d21..771e1247f7 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -562,6 +562,7 @@ libgit_sources = [\n   'transport-helper.c',\n   'transport.c',\n   'tree-diff.c',\n+  'tree-sha256.c',\n   'tree-walk.c',\n   'tree.c',\n   'unpack-trees.c',\ndiff --git a/t/helper/meson.build b/t/helper/meson.build\nindex 3235f10ab8..6f6a4e0a42 100644\n--- a/t/helper/meson.build\n+++ b/t/helper/meson.build\n@@ -72,6 +72,7 @@ test_tool_sources = [\n   'test-synthesize.c',\n   'test-tool.c',\n   'test-trace2.c',\n+  'test-tree-sha256.c',\n   'test-truncate.c',\n   'test-userdiff.c',\n   'test-wildmatch.c',\ndiff --git a/t/helper/test-tool.c b/t/helper/test-tool.c\nindex b71a22b43b..d3452f7297 100644\n--- a/t/helper/test-tool.c\n+++ b/t/helper/test-tool.c\n@@ -84,6 +84,7 @@ static struct test_cmd cmds[] = {\n \t{ \"subprocess\", cmd__subprocess },\n \t{ \"synthesize\", cmd__synthesize },\n \t{ \"trace2\", cmd__trace2 },\n+\t{ \"tree-sha256\", cmd__tree_sha256 },\n \t{ \"truncate\", cmd__truncate },\n \t{ \"userdiff\", cmd__userdiff },\n \t{ \"xml-encode\", cmd__xml_encode },\ndiff --git a/t/helper/test-tool.h b/t/helper/test-tool.h\nindex f2885b33d5..410d71f6a9 100644\n--- a/t/helper/test-tool.h\n+++ b/t/helper/test-tool.h\n@@ -77,6 +77,7 @@ int cmd__submodule_nested_repo_config(int argc, const char **argv);\n int cmd__subprocess(int argc, const char **argv);\n int cmd__synthesize(int argc, const char **argv);\n int cmd__trace2(int argc, const char **argv);\n+int cmd__tree_sha256(int argc, const char **argv);\n int cmd__truncate(int argc, const char **argv);\n int cmd__userdiff(int argc, const char **argv);\n int cmd__xml_encode(int argc, const char **argv);\ndiff --git a/t/helper/test-tree-sha256.c b/t/helper/test-tree-sha256.c\nnew file mode 100644\nindex 0000000000..d3fca5c0b7\n--- /dev/null\n+++ b/t/helper/test-tree-sha256.c\n@@ -0,0 +1,31 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n+#include \"test-tool.h\"\n+#include \"git-compat-util.h\"\n+#include \"hash.h\"\n+#include \"object-name.h\"\n+#include \"repository.h\"\n+#include \"setup.h\"\n+#include \"strbuf.h\"\n+#include \"tree-sha256.h\"\n+\n+int cmd__tree_sha256(int argc, const char **argv)\n+{\n+\tstruct object_id oid;\n+\tstruct strbuf hex = STRBUF_INIT;\n+\tint ret = 0;\n+\n+\tsetup_git_directory(the_repository);\n+\tif (argc != 2)\n+\t\tdie(\"usage: test-tool tree-sha256 <tree-ish>\");\n+\tif (repo_get_oid(the_repository, argv[1], &oid))\n+\t\tdie(\"not a valid object name: %s\", argv[1]);\n+\n+\tif (tree_sha256_hex(the_repository, &oid, &hex))\n+\t\tret = 1;\n+\telse\n+\t\tputs(hex.buf);\n+\n+\tstrbuf_release(&hex);\n+\treturn ret;\n+}\ndiff --git a/t/meson.build b/t/meson.build\nindex 3ca7b27104..8ff3dbe69d 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -172,6 +172,7 @@ integration_tests = [\n   't1015-read-index-unmerged.sh',\n   't1016-compatObjectFormat.sh',\n   't1017-cat-file-remote-object-info.sh',\n+  't1018-tree-sha256.sh',\n   't1020-subdirectory.sh',\n   't1022-read-tree-partial-clone.sh',\n   't1050-large.sh',\ndiff --git a/t/t1018-tree-sha256.sh b/t/t1018-tree-sha256.sh\nnew file mode 100755\nindex 0000000000..ff2edd159e\n--- /dev/null\n+++ b/t/t1018-tree-sha256.sh\n@@ -0,0 +1,123 @@\n+#!/bin/sh\n+\n+test_description='SHA-256 digest of the contents of a tree'\n+\n+. ./test-lib.sh\n+\n+# Recompute the tree-sha256 of <rev> in repository <dir> by hand: one\n+# \"<sha256 of content> <path>\" record per file and \"<digest> <path>/\"\n+# per submodule, sorted by path, NUL-terminated and hashed together.\n+expect_tree_sha256 () {\n+\tgit -C \"$1\" ls-tree -r --format=\"%(objectmode) %(objectname) %(path)\" \"$2\" |\n+\twhile read mode oid path\n+\tdo\n+\t\tcase \"$mode\" in\n+\t\t160000)\n+\t\t\tprintf \"%s %s/\\n\" \"$(expect_tree_sha256 \"$1/$path\" \"$oid\")\" \"$path\" ;;\n+\t\t*)\n+\t\t\tprintf \"%s %s\\n\" \"$(git -C \"$1\" cat-file blob \"$oid\" |\n+\t\t\t\t\t    test-tool sha256)\" \"$path\" ;;\n+\t\tesac\n+\tdone |\n+\tLC_ALL=C sort -t \" \" -k2 |\n+\ttr \"\\n\" \"\\000\" |\n+\ttest-tool sha256\n+}\n+\n+test_expect_success 'setup' '\n+\tgit config --global protocol.file.allow always &&\n+\n+\tgit init inner &&\n+\ttest_commit -C inner inner-file &&\n+\tgit init sub &&\n+\ttest_commit -C sub sub-file &&\n+\tgit -C sub submodule add ../inner inner &&\n+\tgit -C sub commit -m \"add inner\" &&\n+\n+\tmkdir -p a/deeper &&\n+\techo one >a/deeper/file &&\n+\techo two >a.b &&\n+\techo exe >exe &&\n+\tgit add a a.b exe &&\n+\ttest_ln_s_add a.b link &&\n+\tgit commit -m initial\n+'\n+\n+test_expect_success 'digest of files, directories and symlinks' '\n+\texpect_tree_sha256 . HEAD >expect &&\n+\ttest-tool tree-sha256 HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'commits, tags and trees give the same digest' '\n+\tgit tag -a -m tag v1 &&\n+\ttest-tool tree-sha256 v1 >tag &&\n+\ttest-tool tree-sha256 HEAD^{tree} >tree &&\n+\ttest_cmp expect tag &&\n+\ttest_cmp expect tree\n+'\n+\n+test_expect_success 'file mode is not part of the digest' '\n+\ttest_chmod +x exe &&\n+\tgit commit -m executable &&\n+\ttest-tool tree-sha256 HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'content and paths are' '\n+\techo changed >a/deeper/file &&\n+\tgit commit -a -m changed &&\n+\ttest-tool tree-sha256 HEAD >changed &&\n+\t! test_cmp expect changed &&\n+\n+\tgit mv a.b a.c &&\n+\tgit commit -m renamed &&\n+\ttest-tool tree-sha256 HEAD >renamed &&\n+\t! test_cmp changed renamed\n+'\n+\n+test_expect_success 'submodules are hashed recursively' '\n+\tgit submodule add ./sub sub &&\n+\tgit submodule update --init --recursive &&\n+\tgit commit -m \"add sub\" &&\n+\texpect_tree_sha256 . HEAD >expect &&\n+\ttest-tool tree-sha256 HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'submodule contents are part of the digest' '\n+\ttest_commit -C sub/inner more &&\n+\tgit -C sub commit -a -m \"update inner\" &&\n+\tgit commit -a -m \"update sub\" &&\n+\ttest-tool tree-sha256 HEAD >updated &&\n+\t! test_cmp expect updated &&\n+\texpect_tree_sha256 . HEAD >expect &&\n+\ttest_cmp expect updated\n+'\n+\n+test_expect_success 'submodules are read from their gitdir without a worktree' '\n+\tmv sub/inner inner.away &&\n+\ttest_when_finished \"mv inner.away sub/inner\" &&\n+\ttest-tool tree-sha256 HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'unavailable submodules are an error' '\n+\tmv sub/inner inner.away &&\n+\tmv sub/.git/modules/inner inner.git.away &&\n+\ttest_when_finished \"mv inner.away sub/inner && mv inner.git.away sub/.git/modules/inner\" &&\n+\ttest_must_fail test-tool tree-sha256 HEAD 2>err &&\n+\ttest_grep \"sub/inner (not checked out)\" err\n+'\n+\n+test_expect_success 'submodule missing the pinned commit is an error' '\n+\ttree=$(printf \"160000 commit %s\\tinner\\n\" $(test_oid deadbeef) |\n+\t       git -C sub mktree) &&\n+\t(\n+\t\tcd sub &&\n+\t\ttest_must_fail test-tool tree-sha256 $tree 2>err &&\n+\t\ttest_grep \"inner (checked out, but missing commit $(test_oid deadbeef))\" err\n+\t)\n+'\n+\n+test_done\ndiff --git a/tree-sha256.c b/tree-sha256.c\nnew file mode 100644\nindex 0000000000..fb58962232\n--- /dev/null\n+++ b/tree-sha256.c\n@@ -0,0 +1,238 @@\n+#include \"git-compat-util.h\"\n+#include \"tree-sha256.h\"\n+#include \"commit.h\"\n+#include \"gettext.h\"\n+#include \"hash.h\"\n+#include \"hex.h\"\n+#include \"object.h\"\n+#include \"odb.h\"\n+#include \"oid-array.h\"\n+#include \"pathspec.h\"\n+#include \"repository.h\"\n+#include \"string-list.h\"\n+#include \"strbuf.h\"\n+#include \"tree.h\"\n+\n+struct record {\n+\tchar *path; /* submodules carry a trailing '/' */\n+\tstruct object_id oid;\n+\tunsigned submodule:1;\n+};\n+\n+struct collect {\n+\tstruct record *items;\n+\tsize_t nr, alloc;\n+};\n+\n+struct walk {\n+\t/* \"<path> (<reason>)\" for each submodule that can't be hashed */\n+\tstruct string_list missing;\n+};\n+\n+static int collect_entry(const struct object_id *oid, struct strbuf *base,\n+\t\t\t const char *pathname, unsigned mode, void *context)\n+{\n+\tstruct collect *c = context;\n+\tstruct record *rec;\n+\n+\tif (S_ISDIR(mode))\n+\t\treturn READ_TREE_RECURSIVE;\n+\n+\tALLOC_GROW(c->items, c->nr + 1, c->alloc);\n+\trec = &c->items[c->nr++];\n+\toidcpy(&rec->oid, oid);\n+\trec->submodule = S_ISGITLINK(mode);\n+\trec->path = xstrfmt(\"%.*s%s%s\", (int)base->len, base->buf, pathname,\n+\t\t\t    rec->submodule ? \"/\" : \"\");\n+\treturn 0;\n+}\n+\n+static int record_cmp(const void *a_, const void *b_)\n+{\n+\tconst struct record *a = a_, *b = b_;\n+\treturn strcmp(a->path, b->path);\n+}\n+\n+static int hash_tree(struct repository *r, const struct object_id *oid,\n+\t\t     const char *prefix, struct oid_array *chain,\n+\t\t     struct walk *walk, unsigned char *digest);\n+\n+static int in_chain(const struct oid_array *chain, const struct object_id *oid)\n+{\n+\tfor (size_t i = 0; i < chain->nr; i++)\n+\t\tif (oideq(&chain->oid[i], oid))\n+\t\t\treturn 1;\n+\treturn 0;\n+}\n+\n+/*\n+ * Hash the submodule of \"r\" at \"path\", pinned at \"commit\", and append\n+ * its digest in hex to \"out\". \"treeish\" is the tree \"path\" was found\n+ * in, which is where .gitmodules is read from if the submodule's\n+ * gitdir isn't at \"path\". \"full\" is the path from the top repository,\n+ * for messages.\n+ *\n+ * Returns 1 if the submodule is unavailable (recording why in\n+ * walk->missing), -1 on other errors and 0 on success.\n+ */\n+static int hash_submodule(struct repository *r, const struct object_id *treeish,\n+\t\t\t  const char *path, const char *full,\n+\t\t\t  const struct object_id *commit,\n+\t\t\t  struct oid_array *chain, struct walk *walk,\n+\t\t\t  struct strbuf *out)\n+{\n+\tconst struct git_hash_algo *sha256 = &hash_algos[GIT_HASH_SHA256];\n+\tunsigned char digest[GIT_MAX_RAWSZ];\n+\tstruct repository sub;\n+\tstruct strbuf sub_prefix = STRBUF_INIT;\n+\tint ret;\n+\n+\tif (repo_submodule_init(&sub, r, path, treeish)) {\n+\t\tstring_list_append_nodup(&walk->missing, xstrfmt(\"%s (%s)\", full,\n+\t\t\t\t    _(\"not checked out\")));\n+\t\treturn 1;\n+\t}\n+\tif (!odb_has_object(sub.objects, commit, 0)) {\n+\t\tstring_list_append_nodup(&walk->missing, xstrfmt(\"%s (%s %s)\", full,\n+\t\t\t\t    _(\"checked out, but missing commit\"),\n+\t\t\t\t    oid_to_hex(commit)));\n+\t\trepo_clear(&sub);\n+\t\treturn 1;\n+\t}\n+\n+\tstrbuf_addf(&sub_prefix, \"%s/\", full);\n+\toid_array_append(chain, commit);\n+\tret = hash_tree(&sub, commit, sub_prefix.buf, chain, walk, digest);\n+\tchain->nr--;\n+\tif (!ret)\n+\t\tstrbuf_addstr(out, hash_to_hex_algop(digest, sha256));\n+\n+\tstrbuf_release(&sub_prefix);\n+\trepo_clear(&sub);\n+\treturn ret;\n+}\n+\n+/*\n+ * Hash one tree of repository \"r\". \"prefix\" is the path of \"r\" from the\n+ * top repository (empty, or ending in '/'), and \"chain\" holds the\n+ * commits of the submodules being hashed above this one.\n+ */\n+static int hash_tree(struct repository *r, const struct object_id *oid,\n+\t\t     const char *prefix, struct oid_array *chain,\n+\t\t     struct walk *walk, unsigned char *digest)\n+{\n+\tconst struct git_hash_algo *sha256 = &hash_algos[GIT_HASH_SHA256];\n+\tstruct git_hash_ctx outer;\n+\tstruct collect c = { 0 };\n+\tstruct pathspec pathspec = { 0 };\n+\tstruct strbuf value = STRBUF_INIT;\n+\tstruct tree *tree;\n+\tint ret = 0;\n+\n+\ttree = repo_parse_tree_indirect(r, oid);\n+\tif (!tree)\n+\t\treturn error(_(\"unable to read tree for %s in %s\"),\n+\t\t\t     oid_to_hex(oid), *prefix ? prefix : \".\");\n+\tif (read_tree(r, tree, &pathspec, collect_entry, &c))\n+\t\treturn error(_(\"unable to read tree %s\"),\n+\t\t\t     oid_to_hex(&tree->object.oid));\n+\tQSORT(c.items, c.nr, record_cmp);\n+\n+\tgit_hash_init(&outer, sha256);\n+\tfor (size_t i = 0; i < c.nr; i++) {\n+\t\tstruct record *rec = &c.items[i];\n+\n+\t\tstrbuf_reset(&value);\n+\t\tif (!rec->submodule) {\n+\t\t\tstruct git_hash_ctx ctx;\n+\t\t\tunsigned char blob_digest[GIT_MAX_RAWSZ];\n+\t\t\tenum object_type type;\n+\t\t\tsize_t size;\n+\t\t\tvoid *data;\n+\n+\t\t\tdata = odb_read_object(r->objects, &rec->oid, &type, &size);\n+\t\t\tif (!data || type != OBJ_BLOB) {\n+\t\t\t\tfree(data);\n+\t\t\t\tret = error(_(\"unable to read blob %s for %s%s\"),\n+\t\t\t\t\t    oid_to_hex(&rec->oid), prefix, rec->path);\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\tgit_hash_init(&ctx, sha256);\n+\t\t\tgit_hash_update(&ctx, data, size);\n+\t\t\tgit_hash_final(blob_digest, &ctx);\n+\t\t\tfree(data);\n+\t\t\tstrbuf_addstr(&value, hash_to_hex_algop(blob_digest, sha256));\n+\t\t} else if (in_chain(chain, &rec->oid)) {\n+\t\t\t/*\n+\t\t\t * A commit can't contain itself without a hash\n+\t\t\t * collision, but don't rely on that to stop.\n+\t\t\t */\n+\t\t\tstrbuf_addf(&value, \"cycle:%s\", oid_to_hex(&rec->oid));\n+\t\t} else {\n+\t\t\tchar *path = xstrndup(rec->path, strlen(rec->path) - 1);\n+\t\t\tchar *full = xstrfmt(\"%s%s\", prefix, path);\n+\t\t\tint res = hash_submodule(r, &tree->object.oid, path, full, &rec->oid,\n+\t\t\t\t\t\t chain, walk, &value);\n+\t\t\tfree(path);\n+\t\t\tfree(full);\n+\t\t\tif (res < 0) {\n+\t\t\t\tret = -1;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\n+\t\tgit_hash_update(&outer, value.buf, value.len);\n+\t\tgit_hash_update(&outer, \" \", 1);\n+\t\tgit_hash_update(&outer, rec->path, strlen(rec->path));\n+\t\tgit_hash_update(&outer, \"\", 1);\n+\t}\n+\tgit_hash_final(digest, &outer);\n+\n+\tfor (size_t i = 0; i < c.nr; i++)\n+\t\tfree(c.items[i].path);\n+\tfree(c.items);\n+\tstrbuf_release(&value);\n+\treturn ret;\n+}\n+\n+int tree_sha256_hex(struct repository *r, const struct object_id *oid,\n+\t\t    struct strbuf *hex)\n+{\n+\tconst struct git_hash_algo *sha256 = &hash_algos[GIT_HASH_SHA256];\n+\tunsigned char digest[GIT_MAX_RAWSZ];\n+\tstruct walk walk = { .missing = STRING_LIST_INIT_DUP };\n+\tstruct oid_array chain = OID_ARRAY_INIT;\n+\tstruct commit *top;\n+\tstruct tree *tree;\n+\tint ret;\n+\n+\ttree = repo_parse_tree_indirect(r, oid);\n+\tif (!tree)\n+\t\treturn error(_(\"cannot compute %s: %s does not point to a tree\"),\n+\t\t\t     TREE_SHA256_HEADER, oid_to_hex(oid));\n+\n+\ttop = lookup_commit_reference_gently(r, oid, 1);\n+\tif (top)\n+\t\toid_array_append(&chain, &top->object.oid);\n+\n+\tret = hash_tree(r, &tree->object.oid, \"\", &chain, &walk, digest);\n+\n+\tif (!ret && walk.missing.nr) {\n+\t\tstruct strbuf list = STRBUF_INIT;\n+\n+\t\tstring_list_sort(&walk.missing);\n+\t\tfor (size_t i = 0; i < walk.missing.nr; i++)\n+\t\t\tstrbuf_addf(&list, \"\\n  %s\", walk.missing.items[i].string);\n+\t\tret = error(_(\"%\"PRIuMAX\" submodule(s) are not available, and \"\n+\t\t\t      \"a %s can't be computed without their tree hashes:%s\\n\"\n+\t\t\t      \"Check them out with `git submodule update --init --recursive`.\"),\n+\t\t\t    (uintmax_t)walk.missing.nr, TREE_SHA256_HEADER, list.buf);\n+\t\tstrbuf_release(&list);\n+\t}\n+\tif (!ret)\n+\t\tstrbuf_addstr(hex, hash_to_hex_algop(digest, sha256));\n+\n+\tstring_list_clear(&walk.missing, 0);\n+\toid_array_clear(&chain);\n+\treturn ret;\n+}\ndiff --git a/tree-sha256.h b/tree-sha256.h\nnew file mode 100644\nindex 0000000000..6d3e5018aa\n--- /dev/null\n+++ b/tree-sha256.h\n@@ -0,0 +1,30 @@\n+#ifndef TREE_SHA256_H\n+#define TREE_SHA256_H\n+\n+struct repository;\n+struct object_id;\n+struct strbuf;\n+\n+/* The header that carries the digest in signed commits and tags. */\n+#define TREE_SHA256_HEADER \"tree-sha256\"\n+\n+/*\n+ * Compute the tree-sha256 of the tree reachable from \"oid\" (a tree,\n+ * commit or tag) and append it to \"hex\" as 64 lowercase hex digits.\n+ *\n+ * Every blob and symlink in the tree, recursively, contributes one\n+ * record \"<hex sha256 of content> SP <path> NUL\". Every submodule\n+ * contributes \"<hex tree-sha256 of submodule> SP <path>/ NUL\", hashed\n+ * from the checked-out submodule's own repository at the commit the\n+ * superproject pins; a submodule whose commit is already being hashed\n+ * further up the chain is recorded as \"cycle:<commit> SP <path>/ NUL\".\n+ * Records are sorted by path (byte order) and the digest is SHA-256\n+ * over their concatenation.\n+ *\n+ * Returns 0 on success. On failure (for example a submodule that is\n+ * not checked out) reports every problem with error() and returns -1.\n+ */\n+int tree_sha256_hex(struct repository *r, const struct object_id *oid,\n+\t\t    struct strbuf *hex);\n+\n+#endif /* TREE_SHA256_H */\n-- \n2.50.1 (Apple Git-155)\n\n"},{"id":"553916","messageId":"20261002081846.25144-3-scott@gitbutler.net","threadId":"66444","inReplyTo":"20261002081846.25144-1-scott@gitbutler.net","subject":"[RFC PATCH 2/4] tag: add --hash=sha256 to sign a tree-sha256 header","fromName":"Scott Chacon","fromEmail":"scott@gitbutler.net","sentAt":"2026-10-02T08:18:44Z","receivedAt":"2026-10-02T08:18:52Z","isPatch":true,"body":"Teach \"git tag -s\" and \"git tag -u\" a \"--hash=sha256\" option that\nputs the tree-sha256 of the tagged tree in a header after \"tagger\":\n\n  object <commit>\n  type commit\n  tag <name>\n  tagger <ident>\n  tree-sha256 <hex>\n\nBeing in the header, it is part of the signed payload, and so the\nsignature now covers the contents of the tagged tree directly. \n\n\"--hash=sha256\" without signing is an error, as there is nothing to\ngain from an unsigned digest. It also makes \"git tag\" create a tag\nobject, so that it isn't silently dropped when making a lightweight\ntag. \"--hash=none\" is accepted so that a later patch can let it\noverride a configured default.\n\n---\n Documentation/git-tag.adoc    | 11 ++++-\n builtin/tag.c                 | 29 +++++++++++--\n t/meson.build                 |  1 +\n t/t7032-tree-sha256-signed.sh | 76 +++++++++++++++++++++++++++++++++++\n tree-sha256.c                 |  9 +++++\n tree-sha256.h                 |  6 +++\n 6 files changed, 128 insertions(+), 4 deletions(-)\n create mode 100755 t/t7032-tree-sha256-signed.sh\n\ndiff --git a/Documentation/git-tag.adoc b/Documentation/git-tag.adoc\nindex cea3202fdb..8901090a6d 100644\n--- a/Documentation/git-tag.adoc\n+++ b/Documentation/git-tag.adoc\n@@ -9,7 +9,7 @@ git-tag - Create, list, delete or verify tags\n SYNOPSIS\n --------\n [synopsis]\n-git tag [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>] [-e]\n+git tag [-a | -s | -u <key-id>] [--hash=<algorithm>] [-f] [-m <msg> | -F <file>] [-e]\n \t[(--trailer <token>[(=|:)<value>])...]\n \t<tagname> [<commit> | <object>]\n git tag -d <tagname>...\n@@ -84,6 +84,15 @@ OPTIONS\n \t`gpg.format` configuration variable. See\n \tlinkgit:git-config[1].\n \n+`--hash=<algorithm>`::\n+\tWhen signing, add a `tree-sha256` header holding a SHA-256\n+\tdigest of every file in the tagged object's tree, including the\n+\tcontents of checked-out submodules, so that the signature covers\n+\tthe content directly rather than only its SHA-1 object names.\n+\t_<algorithm>_ is `sha256`, or `none` (the default).\n+\tGiving `--hash=sha256` without signing is an error. All\n+\tsubmodules must be checked out.\n+\n `-f`::\n `--force`::\n \tReplace an existing tag with the given name (instead of failing)\ndiff --git a/builtin/tag.c b/builtin/tag.c\nindex 06c125b53c..9bc4c946d1 100644\n--- a/builtin/tag.c\n+++ b/builtin/tag.c\n@@ -33,9 +33,10 @@\n #include \"write-or-die.h\"\n #include \"object-file-convert.h\"\n #include \"trailer.h\"\n+#include \"tree-sha256.h\"\n \n static const char * const git_tag_usage[] = {\n-\tN_(\"git tag [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>] [-e]\\n\"\n+\tN_(\"git tag [-a | -s | -u <key-id>] [--hash=<algorithm>] [-f] [-m <msg> | -F <file>] [-e]\\n\"\n \t   \"        [(--trailer <token>[(=|:)<value>])...]\\n\"\n \t   \"        <tagname> [<commit> | <object>]\"),\n \tN_(\"git tag -d <tagname>...\"),\n@@ -281,6 +282,7 @@ struct create_tag_options {\n \tunsigned int message_given:1;\n \tunsigned int use_editor:1;\n \tunsigned int sign;\n+\tunsigned int tree_hash;\n \tenum {\n \t\tCLEANUP_NONE,\n \t\tCLEANUP_SPACE,\n@@ -316,11 +318,19 @@ static void create_tag(const struct object_id *object, const char *object_ref,\n \t\t    \"object %s\\n\"\n \t\t    \"type %s\\n\"\n \t\t    \"tag %s\\n\"\n-\t\t    \"tagger %s\\n\\n\",\n+\t\t    \"tagger %s\\n\",\n \t\t    oid_to_hex(object),\n \t\t    type_name(type),\n \t\t    tag,\n \t\t    git_committer_info(IDENT_STRICT));\n+\tif (opt->sign && opt->tree_hash) {\n+\t\tstrbuf_addstr(&header, TREE_SHA256_HEADER \" \");\n+\t\tif (tree_sha256_hex(the_repository, object, &header))\n+\t\t\tdie(_(\"unable to compute %s for %s\"),\n+\t\t\t    TREE_SHA256_HEADER, object_ref);\n+\t\tstrbuf_addch(&header, '\\n');\n+\t}\n+\tstrbuf_addch(&header, '\\n');\n \n \tshould_edit = opt->use_editor || !opt->message_given;\n \tif (should_edit || trailer_args->nr) {\n@@ -468,6 +478,7 @@ int cmd_tag(int argc,\n \tint cmdmode = 0, create_tag_object = 0;\n \tchar *msgfile = NULL;\n \tconst char *keyid = NULL;\n+\tconst char *hash_arg = NULL;\n \tstruct msg_arg msg = { .buf = STRBUF_INIT };\n \tstruct ref_transaction *transaction;\n \tstruct strbuf err = STRBUF_INIT;\n@@ -503,6 +514,8 @@ int cmd_tag(int argc,\n \t\t\t   N_(\"add custom trailer(s)\")),\n \t\tOPT_BOOL('e', \"edit\", &edit_flag, N_(\"force edit of tag message\")),\n \t\tOPT_BOOL('s', \"sign\", &opt.sign, N_(\"annotated and GPG-signed tag\")),\n+\t\tOPT_STRING(0, \"hash\", &hash_arg, N_(\"algorithm\"),\n+\t\t\t   N_(\"sign a tree-sha256 header of the tagged tree (sha256 or none)\")),\n \t\tOPT_CLEANUP(&cleanup_arg),\n \t\tOPT_STRING('u', \"local-user\", &keyid, N_(\"key-id\"),\n \t\t\t\t\tN_(\"use another key to sign the tag\")),\n@@ -556,6 +569,14 @@ int cmd_tag(int argc,\n \n \targc = parse_options(argc, argv, prefix, options, git_tag_usage, 0);\n \n+\tif (hash_arg) {\n+\t\tint tree_hash = parse_signing_hash(hash_arg);\n+\t\tif (tree_hash < 0)\n+\t\t\tdie(_(\"unsupported --hash value '%s' (use 'sha256' or 'none')\"),\n+\t\t\t    hash_arg);\n+\t\topt.tree_hash = tree_hash;\n+\t}\n+\n \tif (!cmdmode) {\n \t\tif (argc == 0)\n \t\t\tcmdmode = 'l';\n@@ -579,7 +600,7 @@ int cmd_tag(int argc,\n \t\tset_signing_key(keyid);\n \t}\n \tcreate_tag_object = (opt.sign || annotate || msg.given || msgfile ||\n-\t\t\t     edit_flag || trailer_args.nr);\n+\t\t\t     edit_flag || trailer_args.nr || opt.tree_hash);\n \n \tif ((create_tag_object || force) && (cmdmode != 0))\n \t\tusage_with_options(git_tag_usage, options);\n@@ -683,6 +704,8 @@ int cmd_tag(int argc,\n \tif (create_tag_object) {\n \t\tif (force_sign_annotate && !annotate)\n \t\t\topt.sign = 1;\n+\t\tif (opt.tree_hash && !opt.sign)\n+\t\t\tdie(_(\"--hash=%s requires a signed tag (-s or -u)\"), hash_arg);\n \t\tpath = repo_git_path(the_repository, \"TAG_EDITMSG\");\n \t\tcreate_tag(&object, object_ref, tag, &buf, &opt, &prev, &object,\n \t\t\t   &trailer_args, path);\ndiff --git a/t/meson.build b/t/meson.build\nindex 8ff3dbe69d..ff83368847 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -877,6 +877,7 @@ integration_tests = [\n   't7012-skip-worktree-writing.sh',\n   't7030-verify-tag.sh',\n   't7031-verify-tag-signed-ssh.sh',\n+  't7032-tree-sha256-signed.sh',\n   't7060-wtstatus.sh',\n   't7061-wtstatus-ignore.sh',\n   't7062-wtstatus-ignorecase.sh',\ndiff --git a/t/t7032-tree-sha256-signed.sh b/t/t7032-tree-sha256-signed.sh\nnew file mode 100755\nindex 0000000000..083f25e665\n--- /dev/null\n+++ b/t/t7032-tree-sha256-signed.sh\n@@ -0,0 +1,76 @@\n+#!/bin/sh\n+\n+test_description='signed tags and commits with a tree-sha256 header'\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+GNUPGHOME_NOT_USED=$GNUPGHOME\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n+\n+# Print the value of the tree-sha256 header of <type> <object>, if any.\n+header_of () {\n+\tgit cat-file \"$1\" \"$2\" >object &&\n+\tsed -n \"/^$/q; s/^tree-sha256 //p\" object\n+}\n+\n+test_expect_success GPGSSH 'setup' '\n+\tgit config --global gpg.format ssh &&\n+\tgit config --global gpg.ssh.allowedSignersFile \"${GPGSSH_ALLOWED_SIGNERS}\" &&\n+\tgit config --global user.signingkey \"${GPGSSH_KEY_PRIMARY}\" &&\n+\tmkdir dir &&\n+\techo one >dir/file &&\n+\techo two >file &&\n+\tgit add dir file &&\n+\ttest_tick &&\n+\tgit commit -m initial &&\n+\ttest-tool tree-sha256 HEAD >expect\n+'\n+\n+test_expect_success GPGSSH 'tag -s --hash=sha256 signs a tree-sha256 header' '\n+\tgit tag -s --hash=sha256 -m release v1 &&\n+\theader_of tag v1 >actual &&\n+\ttest_cmp expect actual &&\n+\tsed -n \"4,5p\" object >lines &&\n+\ttest_grep \"^tagger \" lines &&\n+\ttest_grep \"^tree-sha256 \" lines &&\n+\tgit tag -v v1 &&\n+\tgit fsck --strict\n+'\n+\n+test_expect_success GPGSSH 'tag -u --hash=sha256 signs a tree-sha256 header' '\n+\tgit tag -u \"${GPGSSH_KEY_PRIMARY}\" --hash=sha256 -m release v2 &&\n+\theader_of tag v2 >actual &&\n+\ttest_cmp expect actual &&\n+\tgit tag -v v2\n+'\n+\n+test_expect_success GPGSSH 'tag -s without --hash has no header' '\n+\tgit tag -s -m release v3 &&\n+\theader_of tag v3 >actual &&\n+\ttest_must_be_empty actual &&\n+\tgit tag -s --hash=none -m release v4 &&\n+\theader_of tag v4 >actual &&\n+\ttest_must_be_empty actual\n+'\n+\n+test_expect_success GPGSSH 'tag --hash=sha256 requires signing' '\n+\ttest_must_fail git tag --hash=sha256 -m release v5 2>err &&\n+\ttest_grep \"requires a signed tag\" err &&\n+\ttest_must_fail git tag --hash=sha256 v5 2>err &&\n+\ttest_grep \"requires a signed tag\" err &&\n+\ttest_must_fail git rev-parse --verify v5\n+'\n+\n+test_expect_success GPGSSH 'tag --hash rejects unknown algorithms' '\n+\ttest_must_fail git tag -s --hash=md5 -m release v5 2>err &&\n+\ttest_grep \"unsupported --hash value\" err &&\n+\ttest_must_fail git rev-parse --verify v5\n+'\n+\n+test_expect_success GPGSSH 'tag --hash=sha256 needs an object with a tree' '\n+\ttest_must_fail git tag -s --hash=sha256 -m blob v5 HEAD:file &&\n+\ttest_must_fail git rev-parse --verify v5\n+'\n+\n+test_done\ndiff --git a/tree-sha256.c b/tree-sha256.c\nindex fb58962232..90f0306521 100644\n--- a/tree-sha256.c\n+++ b/tree-sha256.c\n@@ -236,3 +236,12 @@ int tree_sha256_hex(struct repository *r, const struct object_id *oid,\n \toid_array_clear(&chain);\n \treturn ret;\n }\n+\n+int parse_signing_hash(const char *value)\n+{\n+\tif (!strcasecmp(value, \"sha256\"))\n+\t\treturn 1;\n+\tif (!strcasecmp(value, \"none\"))\n+\t\treturn 0;\n+\treturn -1;\n+}\ndiff --git a/tree-sha256.h b/tree-sha256.h\nindex 6d3e5018aa..dc070129ea 100644\n--- a/tree-sha256.h\n+++ b/tree-sha256.h\n@@ -27,4 +27,10 @@ struct strbuf;\n int tree_sha256_hex(struct repository *r, const struct object_id *oid,\n \t\t    struct strbuf *hex);\n \n+/*\n+ * Parse the value of a --hash=<algorithm> option. Returns 1 for\n+ * \"sha256\", 0 for \"none\", and -1 for anything else.\n+ */\n+int parse_signing_hash(const char *value);\n+\n #endif /* TREE_SHA256_H */\n-- \n2.50.1 (Apple Git-155)\n\n"},{"id":"553917","messageId":"20261002081846.25144-4-scott@gitbutler.net","threadId":"66444","inReplyTo":"20261002081846.25144-1-scott@gitbutler.net","subject":"[RFC PATCH 3/4] commit: add --hash=sha256 to sign a tree-sha256 header","fromName":"Scott Chacon","fromEmail":"scott@gitbutler.net","sentAt":"2026-10-02T08:18:45Z","receivedAt":"2026-10-02T08:18:53Z","isPatch":true,"body":"Teach \"git commit -S\" the same \"--hash=sha256\" option as \"git tag\",\nwhich adds the tree-sha256 of the tree being committed as an extra\nheader after \"committer\":\n\n  tree <tree>\n  parent <parent>\n  author <ident>\n  committer <ident>\n  tree-sha256 <hex>\n  gpgsig <signature>\n\nExtra headers are written before the commit is signed, so the\nsignature covers it, and \"git verify-commit\" works as before.\n\nWhen amending, we normally carry over the extra headers of the commit\nbeing amended. Don't do that for tree-sha256, which would be wrong as\nsoon as the tree changes, and add a new one only if the amended commit\nis signed with \"--hash=sha256\".\n\n---\n Documentation/git-commit.adoc | 11 +++++++-\n builtin/commit.c              | 38 ++++++++++++++++++++++++---\n t/t7032-tree-sha256-signed.sh | 49 +++++++++++++++++++++++++++++++++++\n 3 files changed, 93 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc\nindex 8329c1034b..c027de2adb 100644\n--- a/Documentation/git-commit.adoc\n+++ b/Documentation/git-commit.adoc\n@@ -15,7 +15,7 @@ git commit [-a | --interactive | --patch] [-s] [-v] [-u[<mode>]] [--amend]\n \t   [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n \t   [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n \t   [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]\n-\t   [--] [<pathspec>...]\n+\t   [--hash=<algorithm>] [--] [<pathspec>...]\n \n DESCRIPTION\n -----------\n@@ -400,6 +400,15 @@ changes to tracked files.\n \tcountermand both `commit.gpgSign` configuration variable, and\n \tearlier `--gpg-sign`.\n \n+`--hash=<algorithm>`::\n+\tWhen signing, add a `tree-sha256` header holding a SHA-256\n+\tdigest of every file in the commit's tree, including the\n+\tcontents of checked-out submodules, so that the signature covers\n+\tthe content directly rather than only its SHA-1 object names.\n+\t_<algorithm>_ is `sha256`, or `none` (the default).\n+\tGiving `--hash=sha256` without signing is an error. All\n+\tsubmodules must be checked out.\n+\n `--`::\n \tDo not interpret any more arguments as options.\n \ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 840b6b4083..871a2bdcd7 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -43,6 +43,7 @@\n #include \"commit-graph.h\"\n #include \"pretty.h\"\n #include \"trailer.h\"\n+#include \"tree-sha256.h\"\n \n static const char * const builtin_commit_usage[] = {\n \tN_(\"git commit [-a | --interactive | --patch] [-s] [-v] [-u[<mode>]] [--amend]\\n\"\n@@ -52,7 +53,7 @@ static const char * const builtin_commit_usage[] = {\n \t   \"           [--date=<date>] [--cleanup=<mode>] [--[no-]status]\\n\"\n \t   \"           [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]\\n\"\n \t   \"           [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]\\n\"\n-\t   \"           [--] [<pathspec>...]\"),\n+\t   \"           [--hash=<algorithm>] [--] [<pathspec>...]\"),\n \tNULL\n };\n \n@@ -129,7 +130,8 @@ static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;\n static int config_commit_verbose = -1; /* unspecified */\n static int no_post_rewrite, allow_empty_message, pathspec_file_nul;\n static const char *untracked_files_arg, *force_date, *ignore_submodule_arg, *ignored_arg;\n-static const char *sign_commit, *pathspec_from_file;\n+static const char *sign_commit, *pathspec_from_file, *hash_arg;\n+static int tree_hash;\n static struct strvec trailer_args = STRVEC_INIT;\n \n /*\n@@ -1737,6 +1739,8 @@ int cmd_commit(int argc,\n \t\t\t.flags = PARSE_OPT_OPTARG,\n \t\t\t.defval = (intptr_t) \"\",\n \t\t},\n+\t\tOPT_STRING(0, \"hash\", &hash_arg, N_(\"algorithm\"),\n+\t\t\t   N_(\"sign a tree-sha256 header of the committed tree (sha256 or none)\")),\n \t\t/* end commit message options */\n \n \t\tOPT_GROUP(N_(\"Commit contents options\")),\n@@ -1821,6 +1825,14 @@ int cmd_commit(int argc,\n \targc = parse_and_validate_options(argc, argv, builtin_commit_options,\n \t\t\t\t\t  builtin_commit_usage,\n \t\t\t\t\t  prefix, current_head, &s);\n+\tif (hash_arg) {\n+\t\ttree_hash = parse_signing_hash(hash_arg);\n+\t\tif (tree_hash < 0)\n+\t\t\tdie(_(\"unsupported --hash value '%s' (use 'sha256' or 'none')\"),\n+\t\t\t    hash_arg);\n+\t\tif (tree_hash && !sign_commit)\n+\t\t\tdie(_(\"--hash=%s requires a signed commit (-S)\"), hash_arg);\n+\t}\n \tif (trailer_args.nr)\n \t\ttrailer_config_init();\n \n@@ -1928,13 +1940,31 @@ int cmd_commit(int argc,\n \t}\n \n \tif (amend) {\n-\t\tconst char *exclude_gpgsig[3] = { \"gpgsig\", \"gpgsig-sha256\", NULL };\n-\t\textra = read_commit_extra_headers(current_head, exclude_gpgsig);\n+\t\tconst char *exclude[4] = {\n+\t\t\t\"gpgsig\", \"gpgsig-sha256\", TREE_SHA256_HEADER, NULL\n+\t\t};\n+\t\textra = read_commit_extra_headers(current_head, exclude);\n \t} else {\n \t\tstruct commit_extra_header **tail = &extra;\n \t\tappend_merge_tag_headers(parents, &tail);\n \t}\n \n+\tif (sign_commit && tree_hash) {\n+\t\tstruct commit_extra_header **tail = &extra;\n+\t\tstruct strbuf hex = STRBUF_INIT;\n+\n+\t\tif (tree_sha256_hex(the_repository,\n+\t\t\t\t    &the_repository->index->cache_tree->oid, &hex)) {\n+\t\t\trollback_index_files();\n+\t\t\tdie(_(\"unable to compute %s\"), TREE_SHA256_HEADER);\n+\t\t}\n+\t\twhile (*tail)\n+\t\t\ttail = &(*tail)->next;\n+\t\tCALLOC_ARRAY(*tail, 1);\n+\t\t(*tail)->key = xstrdup(TREE_SHA256_HEADER);\n+\t\t(*tail)->value = strbuf_detach(&hex, &(*tail)->len);\n+\t}\n+\n \tif (commit_tree_extended(sb.buf, sb.len, &the_repository->index->cache_tree->oid,\n \t\t\t\t parents, &oid, author_ident.buf, NULL,\n \t\t\t\t sign_commit, extra)) {\ndiff --git a/t/t7032-tree-sha256-signed.sh b/t/t7032-tree-sha256-signed.sh\nindex 083f25e665..44c363b5d2 100755\n--- a/t/t7032-tree-sha256-signed.sh\n+++ b/t/t7032-tree-sha256-signed.sh\n@@ -73,4 +73,53 @@ test_expect_success GPGSSH 'tag --hash=sha256 needs an object with a tree' '\n \ttest_must_fail git rev-parse --verify v5\n '\n \n+test_expect_success GPGSSH 'commit -S --hash=sha256 signs a tree-sha256 header' '\n+\ttest_tick &&\n+\tgit commit --allow-empty -S --hash=sha256 -m signed &&\n+\theader_of commit HEAD >actual &&\n+\ttest_cmp expect actual &&\n+\tgit verify-commit HEAD\n+'\n+\n+test_expect_success GPGSSH 'commit -S without --hash has no header' '\n+\ttest_tick &&\n+\tgit commit --allow-empty -S -m signed &&\n+\theader_of commit HEAD >actual &&\n+\ttest_must_be_empty actual &&\n+\tgit commit --allow-empty -S --hash=none -m signed &&\n+\theader_of commit HEAD >actual &&\n+\ttest_must_be_empty actual\n+'\n+\n+test_expect_success GPGSSH 'commit --hash=sha256 requires signing' '\n+\tgit rev-parse HEAD >before &&\n+\ttest_must_fail git commit --allow-empty --hash=sha256 -m unsigned 2>err &&\n+\ttest_grep \"requires a signed commit\" err &&\n+\ttest_must_fail git commit --allow-empty -S --no-gpg-sign --hash=sha256 \\\n+\t\t-m unsigned 2>err &&\n+\ttest_grep \"requires a signed commit\" err &&\n+\ttest_must_fail git commit --allow-empty -S --hash=md5 -m signed 2>err &&\n+\ttest_grep \"unsupported --hash value\" err &&\n+\tgit rev-parse HEAD >after &&\n+\ttest_cmp before after\n+'\n+\n+test_expect_success GPGSSH 'amending recomputes or drops the header' '\n+\tgit commit --allow-empty -S --hash=sha256 -m signed &&\n+\techo changed >dir/file &&\n+\tgit add dir/file &&\n+\ttest_tick &&\n+\tgit commit --amend -S --hash=sha256 -m amended &&\n+\ttest-tool tree-sha256 HEAD >expect-amended &&\n+\t! test_cmp expect expect-amended &&\n+\theader_of commit HEAD >actual &&\n+\ttest_cmp expect-amended actual &&\n+\tgit verify-commit HEAD &&\n+\n+\ttest_tick &&\n+\tgit commit --amend -m \"amended unsigned\" &&\n+\theader_of commit HEAD >actual &&\n+\ttest_must_be_empty actual\n+'\n+\n test_done\n-- \n2.50.1 (Apple Git-155)\n\n"},{"id":"553918","messageId":"20261002081846.25144-5-scott@gitbutler.net","threadId":"66444","inReplyTo":"20261002081846.25144-1-scott@gitbutler.net","subject":"[RFC PATCH 4/4] gpg: add gpg.treeHash to sign a tree-sha256 header by default","fromName":"Scott Chacon","fromEmail":"scott@gitbutler.net","sentAt":"2026-10-02T08:18:46Z","receivedAt":"2026-10-02T08:18:54Z","isPatch":true,"body":"Someone who wants their signatures to cover the contents of their\ntrees wants it for every tag and commit they sign, and shouldn't have\nto remember \"--hash=sha256\" each time, much as \"tag.gpgSign\" and\n\"commit.gpgSign\" save them from remembering \"-s\" and \"-S\".\n\nAdd \"gpg.treeHash\", which \"git tag\" and \"git commit\" use as the\ndefault for \"--hash\". It is a single variable rather than one for\neach command, since the reason for wanting it is the same for both.\n\nIt only applies to objects that are signed: with it set, unsigned\ncommits and annotated or lightweight tags are made as before, rather\nthan failing as an explicit \"--hash=sha256\" without signing does.\n\"--hash=none\" overrides it.\n\n---\n Documentation/config/gpg.adoc |  6 +++++\n Documentation/git-commit.adoc |  2 +-\n Documentation/git-tag.adoc    |  2 +-\n builtin/commit.c              |  8 +++++++\n builtin/tag.c                 | 14 ++++++++++-\n t/t7032-tree-sha256-signed.sh | 44 +++++++++++++++++++++++++++++++++++\n tree-sha256.h                 |  4 ++--\n 7 files changed, 75 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/config/gpg.adoc b/Documentation/config/gpg.adoc\nindex 240e46c050..6728c13a62 100644\n--- a/Documentation/config/gpg.adoc\n+++ b/Documentation/config/gpg.adoc\n@@ -16,6 +16,12 @@ gpg.format::\n See linkgit:gitformat-signature[5] for the signature format, which differs\n based on the selected `gpg.format`.\n \n+gpg.treeHash::\n+\tWhen set to `sha256`, `git commit` and `git tag` add a\n+\t`tree-sha256` header to every commit and tag they sign, as if\n+\t`--hash=sha256` were given. Defaults to `none`. See the\n+\t`--hash` option in linkgit:git-commit[1] and linkgit:git-tag[1].\n+\n gpg.<format>.program::\n \tUse this to customize the program used for the signing format you\n \tchose. (see `gpg.program` and `gpg.format`) `gpg.program` can still\ndiff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc\nindex c027de2adb..1ed6635eee 100644\n--- a/Documentation/git-commit.adoc\n+++ b/Documentation/git-commit.adoc\n@@ -405,7 +405,7 @@ changes to tracked files.\n \tdigest of every file in the commit's tree, including the\n \tcontents of checked-out submodules, so that the signature covers\n \tthe content directly rather than only its SHA-1 object names.\n-\t_<algorithm>_ is `sha256`, or `none` (the default).\n+\t_<algorithm>_ is `sha256`, or `none` to override `gpg.treeHash`.\n \tGiving `--hash=sha256` without signing is an error. All\n \tsubmodules must be checked out.\n \ndiff --git a/Documentation/git-tag.adoc b/Documentation/git-tag.adoc\nindex 8901090a6d..445be41db1 100644\n--- a/Documentation/git-tag.adoc\n+++ b/Documentation/git-tag.adoc\n@@ -89,7 +89,7 @@ OPTIONS\n \tdigest of every file in the tagged object's tree, including the\n \tcontents of checked-out submodules, so that the signature covers\n \tthe content directly rather than only its SHA-1 object names.\n-\t_<algorithm>_ is `sha256`, or `none` (the default).\n+\t_<algorithm>_ is `sha256`, or `none` to override `gpg.treeHash`.\n \tGiving `--hash=sha256` without signing is an error. All\n \tsubmodules must be checked out.\n \ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 871a2bdcd7..7e56c434db 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1687,6 +1687,14 @@ static int git_commit_config(const char *k, const char *v,\n \t\tsign_commit = git_config_bool(k, v) ? \"\" : NULL;\n \t\treturn 0;\n \t}\n+\tif (!strcmp(k, \"gpg.treehash\")) {\n+\t\tif (!v)\n+\t\t\treturn config_error_nonbool(k);\n+\t\ttree_hash = parse_signing_hash(v);\n+\t\tif (tree_hash < 0)\n+\t\t\treturn error(_(\"invalid value for '%s': '%s'\"), k, v);\n+\t\treturn 0;\n+\t}\n \tif (!strcmp(k, \"commit.verbose\")) {\n \t\tint is_bool;\n \t\tconfig_commit_verbose = git_config_bool_or_int(k, v, ctx->kvi,\ndiff --git a/builtin/tag.c b/builtin/tag.c\nindex 9bc4c946d1..86871317ed 100644\n--- a/builtin/tag.c\n+++ b/builtin/tag.c\n@@ -51,6 +51,7 @@ static const char * const git_tag_usage[] = {\n static unsigned int colopts;\n static int force_sign_annotate;\n static int config_sign_tag = -1; /* unspecified */\n+static int config_tree_hash;\n \n static int list_tags(struct ref_filter *filter, struct ref_sorting *sorting,\n \t\t     struct ref_format *format)\n@@ -223,6 +224,15 @@ static int git_tag_config(const char *var, const char *value,\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(var, \"gpg.treehash\")) {\n+\t\tif (!value)\n+\t\t\treturn config_error_nonbool(var);\n+\t\tconfig_tree_hash = parse_signing_hash(value);\n+\t\tif (config_tree_hash < 0)\n+\t\t\treturn error(_(\"invalid value for '%s': '%s'\"), var, value);\n+\t\treturn 0;\n+\t}\n+\n \tif (!strcmp(var, \"tag.forcesignannotated\")) {\n \t\tforce_sign_annotate = git_config_bool(var, value);\n \t\treturn 0;\n@@ -601,6 +611,8 @@ int cmd_tag(int argc,\n \t}\n \tcreate_tag_object = (opt.sign || annotate || msg.given || msgfile ||\n \t\t\t     edit_flag || trailer_args.nr || opt.tree_hash);\n+\tif (!hash_arg)\n+\t\topt.tree_hash = config_tree_hash;\n \n \tif ((create_tag_object || force) && (cmdmode != 0))\n \t\tusage_with_options(git_tag_usage, options);\n@@ -704,7 +716,7 @@ int cmd_tag(int argc,\n \tif (create_tag_object) {\n \t\tif (force_sign_annotate && !annotate)\n \t\t\topt.sign = 1;\n-\t\tif (opt.tree_hash && !opt.sign)\n+\t\tif (opt.tree_hash && !opt.sign && hash_arg)\n \t\t\tdie(_(\"--hash=%s requires a signed tag (-s or -u)\"), hash_arg);\n \t\tpath = repo_git_path(the_repository, \"TAG_EDITMSG\");\n \t\tcreate_tag(&object, object_ref, tag, &buf, &opt, &prev, &object,\ndiff --git a/t/t7032-tree-sha256-signed.sh b/t/t7032-tree-sha256-signed.sh\nindex 44c363b5d2..5a656a7816 100755\n--- a/t/t7032-tree-sha256-signed.sh\n+++ b/t/t7032-tree-sha256-signed.sh\n@@ -122,4 +122,48 @@ test_expect_success GPGSSH 'amending recomputes or drops the header' '\n \ttest_must_be_empty actual\n '\n \n+test_expect_success GPGSSH 'gpg.treeHash signs the header by default' '\n+\ttest-tool tree-sha256 HEAD >expect &&\n+\ttest_config gpg.treeHash sha256 &&\n+\tgit tag -s -m release v6 &&\n+\theader_of tag v6 >actual &&\n+\ttest_cmp expect actual &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -S -m signed &&\n+\theader_of commit HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success GPGSSH 'gpg.treeHash leaves unsigned objects alone' '\n+\ttest_config gpg.treeHash sha256 &&\n+\tgit tag -a -m annotated v7 &&\n+\theader_of tag v7 >actual &&\n+\ttest_must_be_empty actual &&\n+\tgit tag v8 &&\n+\ttest \"$(git cat-file -t v8)\" = commit &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m unsigned &&\n+\theader_of commit HEAD >actual &&\n+\ttest_must_be_empty actual\n+'\n+\n+test_expect_success GPGSSH '--hash=none overrides gpg.treeHash' '\n+\ttest_config gpg.treeHash sha256 &&\n+\tgit tag -s --hash=none -m release v9 &&\n+\theader_of tag v9 >actual &&\n+\ttest_must_be_empty actual &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -S --hash=none -m signed &&\n+\theader_of commit HEAD >actual &&\n+\ttest_must_be_empty actual\n+'\n+\n+test_expect_success GPGSSH 'invalid gpg.treeHash is an error' '\n+\ttest_config gpg.treeHash md5 &&\n+\ttest_must_fail git tag -s -m release v10 2>err &&\n+\ttest_grep \"invalid value for .gpg.treehash.\" err &&\n+\ttest_must_fail git commit --allow-empty -S -m signed 2>err &&\n+\ttest_grep \"invalid value for .gpg.treehash.\" err\n+'\n+\n test_done\ndiff --git a/tree-sha256.h b/tree-sha256.h\nindex dc070129ea..6d54c2c9f9 100644\n--- a/tree-sha256.h\n+++ b/tree-sha256.h\n@@ -28,8 +28,8 @@ int tree_sha256_hex(struct repository *r, const struct object_id *oid,\n \t\t    struct strbuf *hex);\n \n /*\n- * Parse the value of a --hash=<algorithm> option. Returns 1 for\n- * \"sha256\", 0 for \"none\", and -1 for anything else.\n+ * Parse the value of a --hash=<algorithm> option or of gpg.treeHash.\n+ * Returns 1 for \"sha256\", 0 for \"none\", and -1 for anything else.\n  */\n int parse_signing_hash(const char *value);\n \n-- \n2.50.1 (Apple Git-155)\n\n"},{"id":"553976","messageId":"xmqqtsn4yuku.fsf@gitster.g","threadId":"66444","inReplyTo":"20261002081846.25144-2-scott@gitbutler.net","subject":"Re: [RFC PATCH 1/4] tree-sha256: hash the contents of a tree with SHA-256","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-02T15:45:53Z","receivedAt":"2026-10-02T15:45:56Z","isPatch":true,"body":"Scott Chacon <scott@gitbutler.net> writes:\n\n> Add a way to compute a SHA-256 digest of the contents of a tree that\n> doesn't depend on the object format, so that it can be put in the\n> signed payload. Each blob in the tree, recursively, becomes one record, \n> and the digest is SHA-256 over the records sorted by path:\n>\n>   <hex sha256 of content> SP <path> NUL\n\nWould three trees, one records a blob with a single word \"hello\" at\na path as an executable regular file, another records the same blob\nat the same path but as a non-executable regular file, and the third\nrecords a symbolic link whose target is \"hello\", hash to the same\nresult?  Should they?\n\n> +static int hash_tree(struct repository *r, const struct object_id *oid,\n> +\t\t     const char *prefix, struct oid_array *chain,\n> +\t\t     struct walk *walk, unsigned char *digest)\n> +{\n> +\tconst struct git_hash_algo *sha256 = &hash_algos[GIT_HASH_SHA256];\n> +\tstruct git_hash_ctx outer;\n> +\tstruct collect c = { 0 };\n> +\tstruct pathspec pathspec = { 0 };\n> +\tstruct strbuf value = STRBUF_INIT;\n> +\tstruct tree *tree;\n> +\tint ret = 0;\n> +\n> +\ttree = repo_parse_tree_indirect(r, oid);\n> +\tif (!tree)\n> +\t\treturn error(_(\"unable to read tree for %s in %s\"),\n> +\t\t\t     oid_to_hex(oid), *prefix ? prefix : \".\");\n> +\tif (read_tree(r, tree, &pathspec, collect_entry, &c))\n> +\t\treturn error(_(\"unable to read tree %s\"),\n> +\t\t\t     oid_to_hex(&tree->object.oid));\n> +\tQSORT(c.items, c.nr, record_cmp);\n\nI am somewhat torn but moderately against this sorting there.  If\nwe have two tree objects that would result in the same checkout,\nbut one is corrupt in such a way that whose entries are not sorted\ncorrectly, we want them to hash to a different value to signal that,\ndon't we?\n\n> +\tgit_hash_init(&outer, sha256);\n> +\tfor (size_t i = 0; i < c.nr; i++) {\n> +\t\tstruct record *rec = &c.items[i];\n> +\n> +\t\tstrbuf_reset(&value);\n> +\t\tif (!rec->submodule) {\n> +\t\t\tstruct git_hash_ctx ctx;\n> +\t\t\tunsigned char blob_digest[GIT_MAX_RAWSZ];\n> +\t\t\tenum object_type type;\n> +\t\t\tsize_t size;\n> +\t\t\tvoid *data;\n> +\n> +\t\t\tdata = odb_read_object(r->objects, &rec->oid, &type, &size);\n> +\t\t\tif (!data || type != OBJ_BLOB) {\n> +\t\t\t\tfree(data);\n> +\t\t\t\tret = error(_(\"unable to read blob %s for %s%s\"),\n> +\t\t\t\t\t    oid_to_hex(&rec->oid), prefix, rec->path);\n> +\t\t\t\tbreak;\n> +\t\t\t}\n> +\t\t\tgit_hash_init(&ctx, sha256);\n> +\t\t\tgit_hash_update(&ctx, data, size);\n> +\t\t\tgit_hash_final(blob_digest, &ctx);\n> +\t\t\tfree(data);\n> +\t\t\tstrbuf_addstr(&value, hash_to_hex_algop(blob_digest, sha256));\n\nThis forces us to read the inflated blob contents as a whole in-core\nbefore we hash.  I wonder if we can use the streaming interface like\nhow archive-{tar,zip}.c uses odb_stream_from_object() to read the\ncontents in smaller chunks?  Instead of writing the contents out\nlike they do, we would instead hash the bytes here.\n"},{"id":"553977","messageId":"xmqqo6dcyuf5.fsf@gitster.g","threadId":"66444","inReplyTo":"20261002081846.25144-3-scott@gitbutler.net","subject":"Re: [RFC PATCH 2/4] tag: add --hash=sha256 to sign a tree-sha256 header","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-02T15:49:18Z","receivedAt":"2026-10-02T15:49:23Z","isPatch":true,"body":"Scott Chacon <scott@gitbutler.net> writes:\n\n> @@ -316,11 +318,19 @@ static void create_tag(const struct object_id *object, const char *object_ref,\n>  \t\t    \"object %s\\n\"\n>  \t\t    \"type %s\\n\"\n>  \t\t    \"tag %s\\n\"\n> -\t\t    \"tagger %s\\n\\n\",\n> +\t\t    \"tagger %s\\n\",\n>  \t\t    oid_to_hex(object),\n>  \t\t    type_name(type),\n>  \t\t    tag,\n>  \t\t    git_committer_info(IDENT_STRICT));\n> +\tif (opt->sign && opt->tree_hash) {\n> +\t\tstrbuf_addstr(&header, TREE_SHA256_HEADER \" \");\n> +\t\tif (tree_sha256_hex(the_repository, object, &header))\n> +\t\t\tdie(_(\"unable to compute %s for %s\"),\n> +\t\t\t    TREE_SHA256_HEADER, object_ref);\n> +\t\tstrbuf_addch(&header, '\\n');\n> +\t}\n> +\tstrbuf_addch(&header, '\\n');\n\nThis is a very nice reorganization.  The hardcoded double LF at the\nend was a declaration that we wanted to make it hard to add new\nfields, but it becomes a hindrance when we want to add an optional\nfield.\n"},{"id":"553978","messageId":"xmqqjyo0yu9b.fsf@gitster.g","threadId":"66444","inReplyTo":"20261002081846.25144-1-scott@gitbutler.net","subject":"Re: [RFC PATCH 0/4] sign a SHA-256 digest of the tree in commits and tags","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-02T15:52:48Z","receivedAt":"2026-10-02T15:52:54Z","isPatch":true,"body":"Scott Chacon <scott@gitbutler.net> writes:\n\n> I'm concerned about the ecosystem impact of moving the `git init` default\n> hashing function to SHA-256 in 3.0. I have suggested that it may be more \n> feasible with similar benefits to add the ability to inject an independently\n> calculated and verifiable tree content sha into signed objects instead.\n>\n> This RFC series is meant to demonstrate how this might work.\n\nI have offered a few minor comments on the implementation, but those\nare conditional on the assumption that if this is a good idea, we\nwould want these improvements.  I have not yet formed an opinion on\nthe overall direction.\n\nThanks.\n"},{"id":"554002","messageId":"asAAn8NZwB29WhGR@fruit.crustytoothpaste.net","threadId":"66444","inReplyTo":"20261002081846.25144-1-scott@gitbutler.net","subject":"Re: [RFC PATCH 0/4] sign a SHA-256 digest of the tree in commits and tags","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-10-02T19:06:08Z","receivedAt":"2026-10-02T19:06:15Z","isPatch":true,"body":"On 2026-10-02 at 08:18:42, Scott Chacon wrote:\n> I'm concerned about the ecosystem impact of moving the `git init` default\n> hashing function to SHA-256 in 3.0. I have suggested that it may be more\n> feasible with similar benefits to add the ability to inject an independently\n> calculated and verifiable tree content sha into signed objects instead.\n\nI don't think this is a good idea.  There are lots of reasons it's not,\nbut the simplest one is that Git requires collision resistance because\nit is impossible to store two different colliding blobs.  We don't have\nany such blobs yet, but I fully expect SHA-1 to become as weak as MD5,\nin which case there will be a large number of items that cannot be\nstored in a Git repository.  Even if you don't want to store those\nblobs, there are many people, such as security researchers, who _do_\nwant to store those blobs and that requires a SHA-256 repository.  Your\napproach does nothing to address that problem.\n\nConsequently, we need to make the problem better as soon as possible and\nthat means moving away from SHA-1.  TLS, OpenPGP, and other major\necosystems have already made this transition and we're very far behind\nthe times.  The Canadian government already recommends users to have\nmoved away from SHA-1 and the U.S. government will no longer allow SHA-1\nfor any purpose as of 2030.  I want to be clear that 4 years in the\nlarge business and government sector is nothing.\n\nI'll also add that the design we have is the design we've had for many\nyears and there has been ample opportunity to propose alternative\ndesigns.  The plan for Git 3.0 is around the March timeframe and making\nsubstantial changes now is far too late.  Every major forge has support\nfor SHA-256, whether publicly or in preview, and no forge has support\nfor this design, nor do I anticipate it seeing a lot of traction,\nespecially since we explicitly rejected the kind of half-transition\nyou're proposing for security and other reasons.  Git 3.0 and the\nrequirement for SHA-256 were discussed at Git Merge 2024 in Berlin and\ndiscussion has happened on the list quite a bit since then, so it\nshouldn't be a surprise to anyone.\n\nThe thing you really want is the interoperability work, which can\nautomatically rewrite repositories from one hash algorithm to another\nduring a clone or fetch operation.  Yes, it isn't quite that simple for\nsubmodules, but if you recursively clone the repository and all its\nsubmodules, it should be possible to rewrite it in place, although that\nhasn't been written yet.  That work has not yet been sent upstream\nbecause some of it was written at $DAYJOB, which requires that we use\nOutlook and we all know that Outlook corrupts patches.  However, there\nis some intention for another company to handle the polishing and\nsending, so it should be available sooner or later.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"554158","messageId":"CAP2yMaKF4CRvtfTQDVe51SqEm_DnoVOmED5kcUSg7UvLkBp4Xg@mail.gmail.com","threadId":"66444","inReplyTo":"asAAn8NZwB29WhGR@fruit.crustytoothpaste.net","subject":"Re: [RFC PATCH 0/4] sign a SHA-256 digest of the tree in commits and tags","fromName":"Scott Chacon","fromEmail":"schacon@gmail.com","sentAt":"2026-10-05T09:32:54Z","receivedAt":"2026-10-05T09:33:11Z","isPatch":true,"body":"Hey all,\n\nThere are basically two things to respond to here and that I feel the\nlist should consider before 3.0.\n\nOne is this specific proposal of an independent content hash in a\nsigned header, which I find interesting and potentially helpful\nregarding SHA-1 issues, but not necessarily fundamental.\n\nThe other is if the default object store for 3.0 should be sha256 or sha1.\n\nOn Fri, Oct 2, 2026 at 9:06 PM brian m. carlson\n<sandals@crustytoothpaste.net> wrote:\n>\n> On 2026-10-02 at 08:18:42, Scott Chacon wrote:\n> > I'm concerned about the ecosystem impact of moving the `git init` default\n> > hashing function to SHA-256 in 3.0. I have suggested that it may be more\n> > feasible with similar benefits to add the ability to inject an independently\n> > calculated and verifiable tree content sha into signed objects instead.\n>\n> I don't think this is a good idea.  There are lots of reasons it's not,\n> but the simplest one is that Git requires collision resistance because\n> it is impossible to store two different colliding blobs.  We don't have\n> any such blobs yet, but I fully expect SHA-1 to become as weak as MD5,\n> in which case there will be a large number of items that cannot be\n> stored in a Git repository.  Even if you don't want to store those\n> blobs, there are many people, such as security researchers, who _do_\n> want to store those blobs and that requires a SHA-256 repository.  Your\n> approach does nothing to address that problem.\n\nMy approach was not meant to address that problem, partially because I\nbelieve it to be an incredibly niche problem. For security researchers\nor people over-interpreting NIST guidelines to mean \"use at all\"\nrather than \"use for signatures\", then SHA256 is clearly already a way\nto initiate a Git repository and can be used that way. They can do so\ntoday - making sha256 the default in 3.0 does not help or hinder them.\n\nI don't mean to say we should remove different hash algorithms from\nGit, but that the _default_ should not bifurcate the entire community\nso that security researchers are slightly happier in still mostly\ntheoretical situations.\n\nEven if Git were based on MD5 content hashing, nearly everyone using\nit in nearly every normal scenario would probably be just fine. We can\nsign something that is not based on that hashing function (the basis\nof this series), but overall, the social trust mechanisms of pull\nsources are the predominant security layer, independent of hashing\nfunction.\n\nIt's important to differentiate this, since it's being conflated.\nUsing SHA-1 for signatures is clearly problematic. Using SHA-1 as the\ncontent-addressing hash for it's Merkle DAG is not. The way Git uses\nSHA-1 primarily does not rely on a hash function being collision-free;\nit relies on a hash function being one-way, which SHA-1 is perfectly\ngood for and always will be.\n\nThe only real issue is that it _also_ uses that hash for signature\nintegrity, which means we can solve the main issue simply by not\n_also_ using it for signature integrity.\n\n> Consequently, we need to make the problem better as soon as possible and\n> that means moving away from SHA-1.  TLS, OpenPGP, and other major\n> ecosystems have already made this transition and we're very far behind\n> the times.  The Canadian government already recommends users to have\n> moved away from SHA-1 and the U.S. government will no longer allow SHA-1\n> for any purpose as of 2030.  I want to be clear that 4 years in the\n> large business and government sector is nothing.\n\nI feel like this is arguably overstated. This is conflating \"any\npurpose\" with \"applying cryptographic protection\" / digital\nsignatures. Part of the point of this series was to ensure that\nsigning would be based on SHA-256 and could in theory make signatures\non Git objects compliant with these NIST-style mandates while still\nusing SHA-1 for the more basic odb content-addressing work.\n\nIn other words, I don't believe that these governments and businesses\nban SHA-1 _for any purpose_. They ban it for the use of cryptographic\nprotection and I'm saying that a simpler approach to solving that\nproblem is to re-seperate content addressing hashes from protective\nsignature hashes.\n\nTLS, OpenPGP, etc all mostly stopped using SHA-1 for signatures, sure,\nbut that's because providing security is essentially all those\nprojects do. Governments still let you use modern browsers and\nwebsockets, even though SHA-1 is used in that protocol, specifically\nbecause it doesn't depend on any security properties of SHA-1.\n\nThis series is likewise proposing an alternative, non-SHA-1 based\nsigning and content verification method along with an argument that\nmaybe seperating those concerns is simpler and less\nbackwards-incompatible.\n\n> I'll also add that the design we have is the design we've had for many\n> years and there has been ample opportunity to propose alternative\n> designs.  The plan for Git 3.0 is around the March timeframe and making\n> substantial changes now is far too late.\n\nFirst of all, if you include the compat work, which imho is incredibly\nimportant to this transition being feasible, \"the design that we have\"\nis not even completed yet and is slightly different every time I hear\nit. As recently as 8 months ago, you yourself stated \"We don't believe\nanyone is getting useful use out of the interoperability code in its\ncurrent state\" [1] and I can verify that this is still the case -\ninterop is currently completely unusable.\n\nThe point of my tree-sha256 series is to actually massively simplify\nthe work remaining and user experience impact. I'm saying \"don't make\nit the default\", which means that the entire ecosystem doesn't need to\nY2K everything for the next 5 months. Even in Git core, there are\nstill _substantial_ changes to make for the compat stuff, if I'm not\nmistaken. If pack index v3 isn't in core now, do you think it's going\nto be in libgit2 and gix and JGit and whatever by March? Not even the\nstuff that landed here 6 years ago is in JGit today.\n\nAs for the late hour comment, I've felt that this \"flag day\" hard cut\nhas been a rather impractical approach to this problem for a while now\nand I have mentioned it to several of you in person in the past.\nHowever, I thought maybe some clever solution would come up over the\nlast two years, but seeing Emily's talk at Git Merge, this close to\nthe proposed cutover, convinced me that it's going to be a usability\nnightmare for everyone. And as above stated, I'm not convinced that\nthis is anywhere near valuable enough of an outcome for the cost and\ndifficulty associated.\n\nI also think that a lot of other people would agree, if they had an\nidea that this was coming. I believe that many, many users will be\nsurprised and confused by this when it hits. It turns out that not\nvery many people read the mailing list.\n\n> Every major forge has support\n> for SHA-256, whether publicly or in preview,\n\nNobody has access to this for GitHub, which is where almost all usage\nis and where the kinks could theoretically have been ironed out. If\n3.0 comes out in March, there will have been no time for anyone to\ngive feedback or make substantial changes before everyone is forced\ninto real usage of this highly incompatible change.\n\nSo Bitbucket doesn't, Gerrit doesn't, GitHub doesn't in any practical\nsense (I'm curious if anyone on even this mailing list has access to\nit's \"preview\"). GitLab has it under \"experimental\". I'm hesitant to\nagree that Codeberg or whatever constitutes \"every major forge\".\n\nIf anything, this is one of my biggest problems with this breaking\nchange proposal - it has not been tested in a real way by nearly\n_anyone_, nor are major parts of the transistion plan\n(compatObjectFormat, pack index v3, fetch/push compatibility, compat\nsig verification, etc) fully implemented even a few months out from\nthe cutover.\n\nAs one small but interesting example, I'm honestly fascinated that\nthere is only now a thread here about the GitHub specific usability\nissues [2] with mixed odb repos (between several GitHub-y people,\nnonetheless) that hasn't been previously considered (the \"limbo\"\nidea). This is the kind of thing (among many others, I'm sure) that\nwould come up if people had time to use this at all before a default\nswitch.\n\n> and no forge has support\n> for this design, nor do I anticipate it seeing a lot of traction,\n> especially since we explicitly rejected the kind of half-transition\n> you're proposing for security and other reasons.\n\nOne of the nice things about the design of this particular series is\nthat no forge support is needed. It would work today.\n\nThe new tag/commit header fscks fine and is transferred fine. You\ncan't rebase signatures anyhow, so dropped headers aren't an issue\n(like commit-ids sometimes are). Verification is trusted locally and\nif `verify-commit` and `verify-tag` learn this header too, I'm unclear\nwhat \"forge support\" you think would be needed. New clients add the\nnew, more secure header, new clients verify it properly, old clients\nfall back gracefully.\n\n> Git 3.0 and the\n> requirement for SHA-256 were discussed at Git Merge 2024 in Berlin and\n> discussion has happened on the list quite a bit since then, so it\n> shouldn't be a surprise to anyone.\n\nYes, my objection is late-ish, but again, it's because I'm not\nsatisfied with the transition plan or implementation and I assumed it\nwould have had more time to have some real world usage before the\ncutover.\n\nFurthermore, that's just from someone who has actually been there for\nmany of these discussions. There are a lot of discussions on the list\nthat will be a surprise to _users_.\n\nDo you have any idea how many custom scripts (various kinds of hooks,\nCI scripts, etc) around the world are going to explode on all new\nrepositories because they have `/^[0-9a-f]{40}$/` hard coded\nsomewhere? It will be the first time most users have any idea that Git\n3.0 creates repos with a different structure - errors like that or\n\"fatal: the receiving end does not support this repository's hash\nalgorithm\", or Eclipse simply not working, or a hundred other little\nissues, will flood unsuspecting Git users. Furthermore, in many cases\nit will be _very_ difficult to figure out why exactly this is\nhappening on some repos and not others.\n\nSo yes, it will surprise many, many people.\n\n> The thing you really want is the interoperability work, which can\n> automatically rewrite repositories from one hash algorithm to another\n> during a clone or fetch operation.  Yes, it isn't quite that simple for\n> submodules, but if you recursively clone the repository and all its\n> submodules, it should be possible to rewrite it in place, although that\n> hasn't been written yet.  That work has not yet been sent upstream\n> because some of it was written at $DAYJOB, which requires that we use\n> Outlook and we all know that Outlook corrupts patches.  However, there\n> is some intention for another company to handle the polishing and\n> sending, so it should be available sooner or later.\n\nI think that well thought through, fully implemented and thoroughly\ntested interop work is fundamental and neccesary to a change in the\ndefault object format, yes. I find it confusing, especially for a\nproject so backwards compatibility focused, that this is not a more\nwidely held viewpoint.\n\nYou don't have to accept a version of this series or it's approach\n(though I do believe that some simpler signing strategy change\nfundamentally solves the main cryptographic security issues, including\nNIST-y gov issues), but if nothing else, I would encourage the group\nto ship 3.0 without the SHA-256 default and let it be used more widely\non an opt-in basis, let tools and forges work out the compat issues\nand have time to get fixes and modifications upstream, and if it's\nstill a pressing issue, change the default in 4.0 or whatever.\n\nThanks,\nScott\n\n[1] https://lore.kernel.org/git/20260207200446.2837699-2-sandals@crustytoothpaste.net/\n[2] https://lore.kernel.org/git/20261002224400.GA834158@coredump.intra.peff.net/\n"},{"id":"554170","messageId":"asOa6dgpj0qV5QAU@pks.im","threadId":"66444","inReplyTo":"CAP2yMaKF4CRvtfTQDVe51SqEm_DnoVOmED5kcUSg7UvLkBp4Xg@mail.gmail.com","subject":"Re: [RFC PATCH 0/4] sign a SHA-256 digest of the tree in commits and tags","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-10-05T12:41:13Z","receivedAt":"2026-10-05T12:41:20Z","isPatch":true,"body":"On Mon, Oct 05, 2026 at 11:32:54AM +0200, Scott Chacon wrote:\n> On Fri, Oct 2, 2026 at 9:06 PM brian m. carlson\n> <sandals@crustytoothpaste.net> wrote:\n> > On 2026-10-02 at 08:18:42, Scott Chacon wrote:\n[snip]\n> > Every major forge has support\n> > for SHA-256, whether publicly or in preview,\n> \n> Nobody has access to this for GitHub, which is where almost all usage\n> is and where the kinks could theoretically have been ironed out. If\n> 3.0 comes out in March, there will have been no time for anyone to\n> give feedback or make substantial changes before everyone is forced\n> into real usage of this highly incompatible change.\n> \n> So Bitbucket doesn't, Gerrit doesn't, GitHub doesn't in any practical\n> sense (I'm curious if anyone on even this mailing list has access to\n> it's \"preview\"). GitLab has it under \"experimental\". I'm hesitant to\n> agree that Codeberg or whatever constitutes \"every major forge\".\n> \n> If anything, this is one of my biggest problems with this breaking\n> change proposal - it has not been tested in a real way by nearly\n> _anyone_, nor are major parts of the transistion plan\n> (compatObjectFormat, pack index v3, fetch/push compatibility, compat\n> sig verification, etc) fully implemented even a few months out from\n> the cutover.\n> \n> As one small but interesting example, I'm honestly fascinated that\n> there is only now a thread here about the GitHub specific usability\n> issues [2] with mixed odb repos (between several GitHub-y people,\n> nonetheless) that hasn't been previously considered (the \"limbo\"\n> idea). This is the kind of thing (among many others, I'm sure) that\n> would come up if people had time to use this at all before a default\n> switch.\n\nThe biggest problem I have is that the ecosystem has been entirely\nunwilling to do anything about the SHA-256 move before we announced that\nthis is going to become mandatory. Only then were developers even able\nto convince anybody (especially those paying the wages) to get the time\nto implement support for it.\n\nSo there is some kind of ossification happening in the space. But things\nare finally moving now that the due-date is drawing closer. I would be\nextremely hesitant to change course again and drop this breaking change\nnow that there finally is some movement. Because the only consequence of\nthat would be that the ecosystem will stop working on it again. And even\nmore so, I would even expect that this will make the next time we want\nto do a breaking change exponentially harder as the lesson learned is\nthat nobody needs to do anything.\n\nMaybe I'm too pessimistic about this, but I don't think so. We've been\nworking on this whole transition for almost a decade by now, and only\nnow where we're forcing the ecosystem to adapt are large players like\nGitHub even moving.\n\nPatrick\n"},{"id":"554180","messageId":"CAP2yMaJ+ss9M_27+kBN0q_aFUd-5GNqzQHM2orayKH+enOAG1Q@mail.gmail.com","threadId":"66444","inReplyTo":"asOa6dgpj0qV5QAU@pks.im","subject":"Re: [RFC PATCH 0/4] sign a SHA-256 digest of the tree in commits and tags","fromName":"Scott Chacon","fromEmail":"schacon@gmail.com","sentAt":"2026-10-05T14:16:48Z","receivedAt":"2026-10-05T14:16:48Z","isPatch":true,"body":"Thanks Steiny,\n\nA quick response,\n\nOn Mon, Oct 5, 2026 at 2:41 PM Patrick Steinhardt <ps@pks.im> wrote:\n> The biggest problem I have is that the ecosystem has been entirely\n> unwilling to do anything about the SHA-256 move before we announced that\n> this is going to become mandatory. Only then were developers even able\n> to convince anybody (especially those paying the wages) to get the time\n> to implement support for it.\n\nBit of a simple question, but is it possible that this is because\nnobody really finds it a concerning problem?\n\n> So there is some kind of ossification happening in the space. But things\n> are finally moving now that the due-date is drawing closer. I would be\n> extremely hesitant to change course again and drop this breaking change\n> now that there finally is some movement. Because the only consequence of\n> that would be that the ecosystem will stop working on it again. And even\n> more so, I would even expect that this will make the next time we want\n> to do a breaking change exponentially harder as the lesson learned is\n> that nobody needs to do anything.\n>\n> Maybe I'm too pessimistic about this, but I don't think so. We've been\n> working on this whole transition for almost a decade by now, and only\n> now where we're forcing the ecosystem to adapt are large players like\n> GitHub even moving.\n\nI want to remind everyone here quickly what \"working on this whole\ntransition for a decade\" has looked like, because this seems to be\nphrased like everyone wanted this but GitHub was hesitant and pulled\ninto this important work only by the heroic 3.0 breaking change\ndecision.\n\nGitHub has been essentially the _only one_ pushing this endeavour from\nthe beginning of this problem set.\n\nIf we assume Brian, Haggerty, Peff, Taylor and Derrick have been\nacting on behalf of GitHub, then you Steiny, are essentially the only\nmajor contributor to this project in the last decade that is not\nGitHub/MS (Eric maybe?). Very honestly, nobody else seems to care. GH\nhas single handedly created this issue and then somehow simultaneously\nbeen the blocking factor to it's rollout because it also,\nsimultaneously, does not really find it to be an actually important\nissue. Google maybe helped design the transition plan in 2017, but\nhasn't seemed to care too much since then. Nobody else has really\nweighed in, at least with patches.\n\nScott\n\n"},{"id":"554227","messageId":"asQrWAKQXV9zn1Vq@fruit.crustytoothpaste.net","threadId":"66444","inReplyTo":"CAP2yMaJ+ss9M_27+kBN0q_aFUd-5GNqzQHM2orayKH+enOAG1Q@mail.gmail.com","subject":"Re: [RFC PATCH 0/4] sign a SHA-256 digest of the tree in commits and tags","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-10-05T22:57:29Z","receivedAt":"2026-10-05T22:57:29Z","isPatch":true,"body":"On 2026-10-05 at 14:16:48, Scott Chacon wrote:\n> Thanks Steiny,\n> \n> A quick response,\n\nHey,\n\n> On Mon, Oct 5, 2026 at 2:41 PM Patrick Steinhardt <ps@pks.im> wrote:\n> > The biggest problem I have is that the ecosystem has been entirely\n> > unwilling to do anything about the SHA-256 move before we announced that\n> > this is going to become mandatory. Only then were developers even able\n> > to convince anybody (especially those paying the wages) to get the time\n> > to implement support for it.\n> \n> Bit of a simple question, but is it possible that this is because\n> nobody really finds it a concerning problem?\n\nI think that's an oversimplification.  I think people don't realize that\nGit is using SHA-1 and once SHA-256 is the default they will be very\nmuch in favour of using it.  As I've said elsewhere in the thread, the\nneed to move away from SHA-1 is going to become gradually urgent for a\nlarge segment of major institutions.\n\nI can say that I've also had inquiries from large government agencies\nand corporations and they very much know about SHA-256 and want it.\nIt's also very much desired by many in the open source community based\non feedback that I've received there.\n\nTo respond to what Patrick said, I think in general there is a huge\nreluctance to invest in Git as an open source project and much open\nsource investment is driven by internal corporate needs.  As such,\nthere's been a huge investment in scaling Git and a lot less investment\nin anything else, even if sometimes that ends up with less desirable\noutcomes.  Customers get developers paged if their repositories don't\nscale, but they don't page about SHA-256.  That doesn't mean it's not\nimportant or valuable.\n\n> > So there is some kind of ossification happening in the space. But things\n> > are finally moving now that the due-date is drawing closer. I would be\n> > extremely hesitant to change course again and drop this breaking change\n> > now that there finally is some movement. Because the only consequence of\n> > that would be that the ecosystem will stop working on it again. And even\n> > more so, I would even expect that this will make the next time we want\n> > to do a breaking change exponentially harder as the lesson learned is\n> > that nobody needs to do anything.\n> >\n> > Maybe I'm too pessimistic about this, but I don't think so. We've been\n> > working on this whole transition for almost a decade by now, and only\n> > now where we're forcing the ecosystem to adapt are large players like\n> > GitHub even moving.\n> \n> I want to remind everyone here quickly what \"working on this whole\n> transition for a decade\" has looked like, because this seems to be\n> phrased like everyone wanted this but GitHub was hesitant and pulled\n> into this important work only by the heroic 3.0 breaking change\n> decision.\n> \n> GitHub has been essentially the _only one_ pushing this endeavour from\n> the beginning of this problem set.\n\nI will merely say in this regard that I don't speak in my corporate\ncapacity from this email address, so I don't think I'd like to respond\nto this statement.  Patrick and I and the other contributors have\ndiscussed SHA-256 and Git 3.0 at the Contributor's Summits in 2024,\n2025, and 2026 and so I think there's a good understanding of where\ndifferent people and companies have been contributing to that and other\nefforts.\n\nWhat I will say is that my experience on SHA-256 is that it challenges a\nlot of assumptions that people have built into their code over the years\nand therefore any sort of migration to support SHA-256 involves a lot of\nwork, including substantial code changes and database migrations.  That\nmeans that sometimes people have been doing substantial work behind the\nscenes and it's just not visible until it's done.  You can see how this\nworks by looking at open source projects like libgit2 and gitoxide,\nwhere extensive changes have landed over time.  My experience is that\nreftable is another project where this is the case as well.\n\n> If we assume Brian, Haggerty, Peff, Taylor and Derrick have been\n> acting on behalf of GitHub, then you Steiny, are essentially the only\n> major contributor to this project in the last decade that is not\n> GitHub/MS (Eric maybe?). Very honestly, nobody else seems to care. GH\n> has single handedly created this issue and then somehow simultaneously\n> been the blocking factor to it's rollout because it also,\n> simultaneously, does not really find it to be an actually important\n> issue. Google maybe helped design the transition plan in 2017, but\n> hasn't seemed to care too much since then. Nobody else has really\n> weighed in, at least with patches.\n\nI do want to clarify this, since I think there's a lot of confusion.\nWhen I send contributions or patches from my personal email address,\nthey're personal contributions.  Only if the patches contain my work\naddress (which is extremely rarely) are they in my corporate capacity or\ndone on corporate time.\n\nThe SHA-256 work that I've been doing has almost exclusively been in my\npersonal capacity[0].  There is some of the interoperability work that I\nwas able to do on work time and those patches reflect the appropriate\nemail address and sign-off, but before that I have done almost no\nSHA-256 work on company time.  This work has been done mostly on nights\nand weekends, as with almost all of my other contributions, including on\nthe security list.  I contribute because I like the project and want it\nsucceed, not because I'm paid to do so.\n\nI also want to state that I've received a great amount of assistance and\ncontributions, including reviews, patches, thoughtful ideas, and\nmiscellaneous assistance, from a wide variety of contributors to the\nlist and I could not have done it without them.  Someone who has only\nprovided reviews or design ideas has still aided the SHA-256 project and\nGit as a whole immensely.  Patrick is just one of many people who have\naided in such a way.\n\nAs mentioned earlier, I am of course not going to comment on anything\nrelated to my employer on any of this.  If you want their opinion, you\nshould ask them.\n\n[0] The interested reader may wish to run the following command:\n    git log --format='%ae' | grep -E '^(sandals|bk2204)@' | sort | uniq -c\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"554249","messageId":"CAP8UFD096CdR9MXd+VHk7Zf9rCJEnGTEiBhCc0mJdMmE3U_gOg@mail.gmail.com","threadId":"66444","inReplyTo":"asAAn8NZwB29WhGR@fruit.crustytoothpaste.net","subject":"Re: [RFC PATCH 0/4] sign a SHA-256 digest of the tree in commits and tags","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-10-06T09:00:45Z","receivedAt":"2026-10-06T09:00:45Z","isPatch":true,"body":"On Fri, Oct 2, 2026 at 9:15 PM brian m. carlson\n<sandals@crustytoothpaste.net> wrote:\n\n> The thing you really want is the interoperability work, which can\n> automatically rewrite repositories from one hash algorithm to another\n> during a clone or fetch operation.  Yes, it isn't quite that simple for\n> submodules, but if you recursively clone the repository and all its\n> submodules, it should be possible to rewrite it in place, although that\n> hasn't been written yet.  That work has not yet been sent upstream\n> because some of it was written at $DAYJOB, which requires that we use\n> Outlook and we all know that Outlook corrupts patches.  However, there\n> is some intention for another company to handle the polishing and\n> sending, so it should be available sooner or later.\n\nSorry for the possibly stupid following questions, but I think the\nanswers might help us get a better idea of what might be needed to get\na smoother transition.\n\nAnd yeah, I know that many people have said that merging all your\ninteroperability work should not block Git 3.0. But if it can ensure a\nsmoother transition, we might want to get at least part of it merged\nsoon, and the rest in a good shape, anyway.\n\nIs the current state of the work publicly available somewhere? Or\ncould you make it publicly available somewhere? (Fine if it's only as\npatches in a tarball.)\n\nIs the submodule work the only missing part of the interoperability work?\n\nHow much work is this? (At one point it seemed to me that it was\naround 200 patches.)\n\nIf you were to work full time on upstreaming it, how long would you\nexpect it would take you?\n\nIf some of us could help you, how could we best help?\n\nCould you say which company is interested in helping with this? Would\nthat company be willing to work openly with others on this?\n\nAre there some tests or kinds of automated ways to check that things\nwork as expected under realistic conditions like:\n\n- using real world repos (large ones, old ones, with submodules, etc),\n- mixing a number of new and old clients and servers,\n- interacting with other implementations (JGit, libgit2, gitoxide,\nforges, CI, etc)?\n\nThanks for all your work on this in your free time,\nChristian.\n\n"}]}