git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[RFC PATCH 3/4] commit: add --hash=sha256 to sign a tree-sha256 header

From
SCScott Chacon <scott@gitbutler.net>
Date
Oct 2, 2026, 08:18 UTC
Message-ID
<20261002081846.25144-4-scott@gitbutler.net>
In-Reply-To
<20261002081846.25144-1-scott@gitbutler.net>

Teach "git commit -S" the same "--hash=sha256" option as "git tag", which adds the tree-sha256 of the tree being committed as an extra header after "committer":

  tree <tree>
  parent <parent>
  author <ident>
  committer <ident>
  tree-sha256 <hex>
  gpgsig <signature>

Extra headers are written before the commit is signed, so the signature covers it, and "git verify-commit" works as before.

When amending, we normally carry over the extra headers of the commit being amended. Don't do that for tree-sha256, which would be wrong as soon as the tree changes, and add a new one only if the amended commit is signed with "--hash=sha256".

---
 Documentation/git-commit.adoc | 11 +++++++-
 builtin/commit.c              | 38 ++++++++++++++++++++++++---
 t/t7032-tree-sha256-signed.sh | 49 +++++++++++++++++++++++++++++++++++
 3 files changed, 93 insertions(+), 5 deletions(-)
diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc
index 8329c1034b..c027de2adb 100644
--- a/Documentation/git-commit.adoc
+++ b/Documentation/git-commit.adoc
@@ -15,7 +15,7 @@ git commit [-a | --interactive | --patch] [-s] [-v] [-u[<mode>]] [--amend]
 	   [--date=<date>] [--cleanup=<mode>] [--[no-]status]
 	   [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]
 	   [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]
-	   [--] [<pathspec>...]
+	   [--hash=<algorithm>] [--] [<pathspec>...]
 
 DESCRIPTION
 -----------
@@ -400,6 +400,15 @@ changes to tracked files.
 	countermand both `commit.gpgSign` configuration variable, and
 	earlier `--gpg-sign`.
 
+`--hash=<algorithm>`::
+	When signing, add a `tree-sha256` header holding a SHA-256
+	digest of every file in the commit's tree, including the
+	contents of checked-out submodules, so that the signature covers
+	the content directly rather than only its SHA-1 object names.
+	_<algorithm>_ is `sha256`, or `none` (the default).
+	Giving `--hash=sha256` without signing is an error. All
+	submodules must be checked out.
+
 `--`::
 	Do not interpret any more arguments as options.
 
diff --git a/builtin/commit.c b/builtin/commit.c
index 840b6b4083..871a2bdcd7 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -43,6 +43,7 @@
 #include "commit-graph.h"
 #include "pretty.h"
 #include "trailer.h"
+#include "tree-sha256.h"
 
 static const char * const builtin_commit_usage[] = {
 	N_("git commit [-a | --interactive | --patch] [-s] [-v] [-u[<mode>]] [--amend]\n"
@@ -52,7 +53,7 @@ static const char * const builtin_commit_usage[] = {
 	   "           [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n"
 	   "           [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n"
 	   "           [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]\n"
-	   "           [--] [<pathspec>...]"),
+	   "           [--hash=<algorithm>] [--] [<pathspec>...]"),
 	NULL
 };
 
@@ -129,7 +130,8 @@ static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
 static int config_commit_verbose = -1; /* unspecified */
 static int no_post_rewrite, allow_empty_message, pathspec_file_nul;
 static const char *untracked_files_arg, *force_date, *ignore_submodule_arg, *ignored_arg;
-static const char *sign_commit, *pathspec_from_file;
+static const char *sign_commit, *pathspec_from_file, *hash_arg;
+static int tree_hash;
 static struct strvec trailer_args = STRVEC_INIT;
 
 /*
@@ -1737,6 +1739,8 @@ int cmd_commit(int argc,
 			.flags = PARSE_OPT_OPTARG,
 			.defval = (intptr_t) "",
 		},
+		OPT_STRING(0, "hash", &hash_arg, N_("algorithm"),
+			   N_("sign a tree-sha256 header of the committed tree (sha256 or none)")),
 		/* end commit message options */
 
 		OPT_GROUP(N_("Commit contents options")),
@@ -1821,6 +1825,14 @@ int cmd_commit(int argc,
 	argc = parse_and_validate_options(argc, argv, builtin_commit_options,
 					  builtin_commit_usage,
 					  prefix, current_head, &s);
+	if (hash_arg) {
+		tree_hash = parse_signing_hash(hash_arg);
+		if (tree_hash < 0)
+			die(_("unsupported --hash value '%s' (use 'sha256' or 'none')"),
+			    hash_arg);
+		if (tree_hash && !sign_commit)
+			die(_("--hash=%s requires a signed commit (-S)"), hash_arg);
+	}
 	if (trailer_args.nr)
 		trailer_config_init();
 
@@ -1928,13 +1940,31 @@ int cmd_commit(int argc,
 	}
 
 	if (amend) {
-		const char *exclude_gpgsig[3] = { "gpgsig", "gpgsig-sha256", NULL };
-		extra = read_commit_extra_headers(current_head, exclude_gpgsig);
+		const char *exclude[4] = {
+			"gpgsig", "gpgsig-sha256", TREE_SHA256_HEADER, NULL
+		};
+		extra = read_commit_extra_headers(current_head, exclude);
 	} else {
 		struct commit_extra_header **tail = &extra;
 		append_merge_tag_headers(parents, &tail);
 	}
 
+	if (sign_commit && tree_hash) {
+		struct commit_extra_header **tail = &extra;
+		struct strbuf hex = STRBUF_INIT;
+
+		if (tree_sha256_hex(the_repository,
+				    &the_repository->index->cache_tree->oid, &hex)) {
+			rollback_index_files();
+			die(_("unable to compute %s"), TREE_SHA256_HEADER);
+		}
+		while (*tail)
+			tail = &(*tail)->next;
+		CALLOC_ARRAY(*tail, 1);
+		(*tail)->key = xstrdup(TREE_SHA256_HEADER);
+		(*tail)->value = strbuf_detach(&hex, &(*tail)->len);
+	}
+
 	if (commit_tree_extended(sb.buf, sb.len, &the_repository->index->cache_tree->oid,
 				 parents, &oid, author_ident.buf, NULL,
 				 sign_commit, extra)) {
diff --git a/t/t7032-tree-sha256-signed.sh b/t/t7032-tree-sha256-signed.sh
index 083f25e665..44c363b5d2 100755
--- a/t/t7032-tree-sha256-signed.sh
+++ b/t/t7032-tree-sha256-signed.sh
@@ -73,4 +73,53 @@ test_expect_success GPGSSH 'tag --hash=sha256 needs an object with a tree' '
 	test_must_fail git rev-parse --verify v5
 '
 
+test_expect_success GPGSSH 'commit -S --hash=sha256 signs a tree-sha256 header' '
+	test_tick &&
+	git commit --allow-empty -S --hash=sha256 -m signed &&
+	header_of commit HEAD >actual &&
+	test_cmp expect actual &&
+	git verify-commit HEAD
+'
+
+test_expect_success GPGSSH 'commit -S without --hash has no header' '
+	test_tick &&
+	git commit --allow-empty -S -m signed &&
+	header_of commit HEAD >actual &&
+	test_must_be_empty actual &&
+	git commit --allow-empty -S --hash=none -m signed &&
+	header_of commit HEAD >actual &&
+	test_must_be_empty actual
+'
+
+test_expect_success GPGSSH 'commit --hash=sha256 requires signing' '
+	git rev-parse HEAD >before &&
+	test_must_fail git commit --allow-empty --hash=sha256 -m unsigned 2>err &&
+	test_grep "requires a signed commit" err &&
+	test_must_fail git commit --allow-empty -S --no-gpg-sign --hash=sha256 \
+		-m unsigned 2>err &&
+	test_grep "requires a signed commit" err &&
+	test_must_fail git commit --allow-empty -S --hash=md5 -m signed 2>err &&
+	test_grep "unsupported --hash value" err &&
+	git rev-parse HEAD >after &&
+	test_cmp before after
+'
+
+test_expect_success GPGSSH 'amending recomputes or drops the header' '
+	git commit --allow-empty -S --hash=sha256 -m signed &&
+	echo changed >dir/file &&
+	git add dir/file &&
+	test_tick &&
+	git commit --amend -S --hash=sha256 -m amended &&
+	test-tool tree-sha256 HEAD >expect-amended &&
+	! test_cmp expect expect-amended &&
+	header_of commit HEAD >actual &&
+	test_cmp expect-amended actual &&
+	git verify-commit HEAD &&
+
+	test_tick &&
+	git commit --amend -m "amended unsigned" &&
+	header_of commit HEAD >actual &&
+	test_must_be_empty actual
+'
+
 test_done
-- 
2.50.1 (Apple Git-155)
Previous: Junio C HamanoNext: Scott Chacon
Message 6 of 22 in “sign a SHA-256 digest of the tree in commits and tags”
  1. 0/4 sign a SHA-256 digest of the tree in commits and tagsScott Chacon, Oct 2, 2026
  2. 1/4 tree-sha256: hash the contents of a tree with SHA-256Scott Chacon, Oct 2, 2026
  3. Junio C HamanoOct 2, 2026
  4. 2/4 tag: add --hash=sha256 to sign a tree-sha256 headerScott Chacon, Oct 2, 2026
  5. Junio C HamanoOct 2, 2026
  6. 3/4 commit: add --hash=sha256 to sign a tree-sha256 headerScott Chacon, Oct 2, 2026
  7. 4/4 gpg: add gpg.treeHash to sign a tree-sha256 header by defaultScott Chacon, Oct 2, 2026
  8. Junio C HamanoOct 2, 2026
  9. brian m. carlsonOct 2, 2026
  10. Scott ChaconOct 5, 2026
  11. Patrick SteinhardtOct 5, 2026
  12. Scott ChaconOct 5, 2026
  13. brian m. carlsonOct 5, 2026
  14. Johannes SchindelinOct 6, 2026
  15. Kristoffer HaugsbakkOct 6, 2026
  16. brian m. carlsonOct 6, 2026
  17. Junio C HamanoOct 6, 2026
  18. brian m. carlsonOct 6, 2026
  19. Christian CouderOct 6, 2026
  20. brian m. carlsonOct 6, 2026
  21. Christian CouderOct 7, 2026
  22. brian m. carlsonOct 7, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.