From: Scott Chacon Date: Fri, 02 Oct 2026 08:18:46 GMT Subject: [RFC PATCH 4/4] gpg: add gpg.treeHash to sign a tree-sha256 header by default Message-ID: <20261002081846.25144-5-scott@gitbutler.net> In-Reply-To: <20261002081846.25144-1-scott@gitbutler.net> Someone who wants their signatures to cover the contents of their trees wants it for every tag and commit they sign, and shouldn't have to remember "--hash=sha256" each time, much as "tag.gpgSign" and "commit.gpgSign" save them from remembering "-s" and "-S". Add "gpg.treeHash", which "git tag" and "git commit" use as the default for "--hash". It is a single variable rather than one for each command, since the reason for wanting it is the same for both. It only applies to objects that are signed: with it set, unsigned commits and annotated or lightweight tags are made as before, rather than failing as an explicit "--hash=sha256" without signing does. "--hash=none" overrides it. --- Documentation/config/gpg.adoc | 6 +++++ Documentation/git-commit.adoc | 2 +- Documentation/git-tag.adoc | 2 +- builtin/commit.c | 8 +++++++ builtin/tag.c | 14 ++++++++++- t/t7032-tree-sha256-signed.sh | 44 +++++++++++++++++++++++++++++++++++ tree-sha256.h | 4 ++-- 7 files changed, 75 insertions(+), 5 deletions(-) diff --git a/Documentation/config/gpg.adoc b/Documentation/config/gpg.adoc index 240e46c050..6728c13a62 100644 --- a/Documentation/config/gpg.adoc +++ b/Documentation/config/gpg.adoc @@ -16,6 +16,12 @@ gpg.format:: See linkgit:gitformat-signature[5] for the signature format, which differs based on the selected `gpg.format`. +gpg.treeHash:: + When set to `sha256`, `git commit` and `git tag` add a + `tree-sha256` header to every commit and tag they sign, as if + `--hash=sha256` were given. Defaults to `none`. See the + `--hash` option in linkgit:git-commit[1] and linkgit:git-tag[1]. + gpg..program:: Use this to customize the program used for the signing format you chose. (see `gpg.program` and `gpg.format`) `gpg.program` can still diff --git a/Documentation/git-commit.adoc b/Documentation/git-commit.adoc index c027de2adb..1ed6635eee 100644 --- a/Documentation/git-commit.adoc +++ b/Documentation/git-commit.adoc @@ -405,7 +405,7 @@ changes to tracked files. 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. - __ is `sha256`, or `none` (the default). + __ is `sha256`, or `none` to override `gpg.treeHash`. Giving `--hash=sha256` without signing is an error. All submodules must be checked out. diff --git a/Documentation/git-tag.adoc b/Documentation/git-tag.adoc index 8901090a6d..445be41db1 100644 --- a/Documentation/git-tag.adoc +++ b/Documentation/git-tag.adoc @@ -89,7 +89,7 @@ OPTIONS digest of every file in the tagged object'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. - __ is `sha256`, or `none` (the default). + __ is `sha256`, or `none` to override `gpg.treeHash`. Giving `--hash=sha256` without signing is an error. All submodules must be checked out. diff --git a/builtin/commit.c b/builtin/commit.c index 871a2bdcd7..7e56c434db 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1687,6 +1687,14 @@ static int git_commit_config(const char *k, const char *v, sign_commit = git_config_bool(k, v) ? "" : NULL; return 0; } + if (!strcmp(k, "gpg.treehash")) { + if (!v) + return config_error_nonbool(k); + tree_hash = parse_signing_hash(v); + if (tree_hash < 0) + return error(_("invalid value for '%s': '%s'"), k, v); + return 0; + } if (!strcmp(k, "commit.verbose")) { int is_bool; config_commit_verbose = git_config_bool_or_int(k, v, ctx->kvi, diff --git a/builtin/tag.c b/builtin/tag.c index 9bc4c946d1..86871317ed 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -51,6 +51,7 @@ static const char * const git_tag_usage[] = { static unsigned int colopts; static int force_sign_annotate; static int config_sign_tag = -1; /* unspecified */ +static int config_tree_hash; static int list_tags(struct ref_filter *filter, struct ref_sorting *sorting, struct ref_format *format) @@ -223,6 +224,15 @@ static int git_tag_config(const char *var, const char *value, return 0; } + if (!strcmp(var, "gpg.treehash")) { + if (!value) + return config_error_nonbool(var); + config_tree_hash = parse_signing_hash(value); + if (config_tree_hash < 0) + return error(_("invalid value for '%s': '%s'"), var, value); + return 0; + } + if (!strcmp(var, "tag.forcesignannotated")) { force_sign_annotate = git_config_bool(var, value); return 0; @@ -601,6 +611,8 @@ int cmd_tag(int argc, } create_tag_object = (opt.sign || annotate || msg.given || msgfile || edit_flag || trailer_args.nr || opt.tree_hash); + if (!hash_arg) + opt.tree_hash = config_tree_hash; if ((create_tag_object || force) && (cmdmode != 0)) usage_with_options(git_tag_usage, options); @@ -704,7 +716,7 @@ int cmd_tag(int argc, if (create_tag_object) { if (force_sign_annotate && !annotate) opt.sign = 1; - if (opt.tree_hash && !opt.sign) + if (opt.tree_hash && !opt.sign && hash_arg) die(_("--hash=%s requires a signed tag (-s or -u)"), hash_arg); path = repo_git_path(the_repository, "TAG_EDITMSG"); create_tag(&object, object_ref, tag, &buf, &opt, &prev, &object, diff --git a/t/t7032-tree-sha256-signed.sh b/t/t7032-tree-sha256-signed.sh index 44c363b5d2..5a656a7816 100755 --- a/t/t7032-tree-sha256-signed.sh +++ b/t/t7032-tree-sha256-signed.sh @@ -122,4 +122,48 @@ test_expect_success GPGSSH 'amending recomputes or drops the header' ' test_must_be_empty actual ' +test_expect_success GPGSSH 'gpg.treeHash signs the header by default' ' + test-tool tree-sha256 HEAD >expect && + test_config gpg.treeHash sha256 && + git tag -s -m release v6 && + header_of tag v6 >actual && + test_cmp expect actual && + test_tick && + git commit --allow-empty -S -m signed && + header_of commit HEAD >actual && + test_cmp expect actual +' + +test_expect_success GPGSSH 'gpg.treeHash leaves unsigned objects alone' ' + test_config gpg.treeHash sha256 && + git tag -a -m annotated v7 && + header_of tag v7 >actual && + test_must_be_empty actual && + git tag v8 && + test "$(git cat-file -t v8)" = commit && + test_tick && + git commit --allow-empty -m unsigned && + header_of commit HEAD >actual && + test_must_be_empty actual +' + +test_expect_success GPGSSH '--hash=none overrides gpg.treeHash' ' + test_config gpg.treeHash sha256 && + git tag -s --hash=none -m release v9 && + header_of tag v9 >actual && + test_must_be_empty actual && + test_tick && + git commit --allow-empty -S --hash=none -m signed && + header_of commit HEAD >actual && + test_must_be_empty actual +' + +test_expect_success GPGSSH 'invalid gpg.treeHash is an error' ' + test_config gpg.treeHash md5 && + test_must_fail git tag -s -m release v10 2>err && + test_grep "invalid value for .gpg.treehash." err && + test_must_fail git commit --allow-empty -S -m signed 2>err && + test_grep "invalid value for .gpg.treehash." err +' + test_done diff --git a/tree-sha256.h b/tree-sha256.h index dc070129ea..6d54c2c9f9 100644 --- a/tree-sha256.h +++ b/tree-sha256.h @@ -28,8 +28,8 @@ int tree_sha256_hex(struct repository *r, const struct object_id *oid, struct strbuf *hex); /* - * Parse the value of a --hash= option. Returns 1 for - * "sha256", 0 for "none", and -1 for anything else. + * Parse the value of a --hash= option or of gpg.treeHash. + * Returns 1 for "sha256", 0 for "none", and -1 for anything else. */ int parse_signing_hash(const char *value); -- 2.50.1 (Apple Git-155)