From: Scott Chacon <scott@gitbutler.net>
To: git@vger.kernel.org
Subject: [RFC PATCH 3/4] commit: add --hash=sha256 to sign a tree-sha256 header
Date: Fri, 2 Oct 2026 10:18:45 +0200 [thread overview]
Message-ID: <20261002081846.25144-4-scott@gitbutler.net> (raw)
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)
next prev parent reply other threads:[~2026-10-02 8:18 UTC|newest]
Thread overview: 22+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-02 8:18 [RFC PATCH 0/4] sign a SHA-256 digest of the tree in commits and tags Scott Chacon
2026-10-02 8:18 ` [RFC PATCH 1/4] tree-sha256: hash the contents of a tree with SHA-256 Scott Chacon
2026-10-02 15:45 ` Junio C Hamano
2026-10-02 8:18 ` [RFC PATCH 2/4] tag: add --hash=sha256 to sign a tree-sha256 header Scott Chacon
2026-10-02 15:49 ` Junio C Hamano
2026-10-02 8:18 ` Scott Chacon [this message]
2026-10-02 8:18 ` [RFC PATCH 4/4] gpg: add gpg.treeHash to sign a tree-sha256 header by default Scott Chacon
2026-10-02 15:52 ` [RFC PATCH 0/4] sign a SHA-256 digest of the tree in commits and tags Junio C Hamano
2026-10-02 19:06 ` brian m. carlson
2026-10-05 9:32 ` Scott Chacon
2026-10-05 12:41 ` Patrick Steinhardt
2026-10-05 14:16 ` Scott Chacon
2026-10-05 22:57 ` brian m. carlson
2026-10-06 13:36 ` Johannes Schindelin
2026-10-06 16:16 ` Kristoffer Haugsbakk
2026-10-06 21:55 ` brian m. carlson
2026-10-06 22:38 ` Junio C Hamano
2026-10-06 23:40 ` brian m. carlson
2026-10-06 9:00 ` Christian Couder
2026-10-06 22:26 ` brian m. carlson
2026-10-07 12:26 ` Christian Couder
2026-10-07 21:07 ` brian m. carlson
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261002081846.25144-4-scott@gitbutler.net \
--to=scott@gitbutler.net \
--cc=git@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox