* [PATCH 1/3] builtin/history: sign rewritten commits
2026-07-03 14:50 [PATCH 0/3] history: sign rewritten commits Souma
@ 2026-07-03 14:50 ` Souma
2026-07-03 14:50 ` [PATCH 2/3] doc: document history signing options Souma
2026-07-03 14:50 ` [PATCH 3/3] t345x: cover signed history rewrites Souma
2 siblings, 0 replies; 4+ messages in thread
From: Souma @ 2026-07-03 14:50 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Souma
The history commands create replacement commits directly instead of
using the sequencer or the commit porcelain. As a result, rewritten
commits ignore commit.gpgsign and cannot be signed on demand.
Read the usual signing configuration before parsing history options.
Add the commit-style -S/--gpg-sign knob, and pass the selected
signing key through direct rewrites and replayed descendants.
Signed-off-by: Souma <git@5ouma.me>
---
builtin/history.c | 80 ++++++++++++++++++++++++++++++++++++-----------
replay.c | 13 +++++---
replay.h | 6 ++++
3 files changed, 76 insertions(+), 23 deletions(-)
diff --git a/builtin/history.c b/builtin/history.c
index 091465a59e..8d669cf539 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -25,11 +25,11 @@
#include "wt-status.h"
#define GIT_HISTORY_FIXUP_USAGE \
- N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]")
+ N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]")
#define GIT_HISTORY_REWORD_USAGE \
- N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]")
+ N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]")
#define GIT_HISTORY_SPLIT_USAGE \
- N_("git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]")
+ N_("git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]")
static void change_data_free(void *util, const char *str UNUSED)
{
@@ -98,6 +98,30 @@ enum commit_tree_flags {
COMMIT_TREE_EDIT_MESSAGE = (1 << 0),
};
+static int history_config(const char *var, const char *value,
+ const struct config_context *ctx, void *data)
+{
+ const char **sign_commit = data;
+
+ if (!strcmp(var, "commit.gpgsign")) {
+ *sign_commit = git_config_bool(var, value) ? "" : NULL;
+ return 0;
+ }
+
+ return git_default_config(var, value, ctx, data);
+}
+
+#define OPT_HISTORY_GPG_SIGN(v) { \
+ .type = OPTION_STRING, \
+ .short_name = 'S', \
+ .long_name = "gpg-sign", \
+ .value = (v), \
+ .argh = N_("key-id"), \
+ .help = N_("GPG-sign rewritten commits"), \
+ .flags = PARSE_OPT_OPTARG, \
+ .defval = (intptr_t) "", \
+}
+
static int commit_tree_ext(struct repository *repo,
const char *action,
struct commit *commit_with_message,
@@ -105,6 +129,7 @@ static int commit_tree_ext(struct repository *repo,
const struct object_id *old_tree,
const struct object_id *new_tree,
struct commit **out,
+ const char *sign_commit,
enum commit_tree_flags flags)
{
const char *exclude_gpgsig[] = {
@@ -144,7 +169,7 @@ static int commit_tree_ext(struct repository *repo,
ret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree,
parents, &rewritten_commit_oid, original_author,
- NULL, NULL, original_extra_headers);
+ NULL, sign_commit, original_extra_headers);
if (ret < 0)
goto out;
@@ -160,7 +185,8 @@ static int commit_tree_ext(struct repository *repo,
static int commit_tree_with_edited_message(struct repository *repo,
const char *action,
struct commit *original,
- struct commit **out)
+ struct commit **out,
+ const char *sign_commit)
{
struct object_id parent_tree_oid;
const struct object_id *tree_oid;
@@ -181,7 +207,8 @@ static int commit_tree_with_edited_message(struct repository *repo,
}
return commit_tree_ext(repo, action, original, original->parents,
- &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);
+ &parent_tree_oid, tree_oid, out, sign_commit,
+ COMMIT_TREE_EDIT_MESSAGE);
}
enum ref_action {
@@ -339,11 +366,13 @@ static int handle_reference_updates(struct rev_info *revs,
struct commit *rewritten,
const char *reflog_msg,
int dry_run,
+ const char *sign_commit,
enum replay_empty_commit_action empty)
{
const struct name_decoration *decoration;
struct replay_revisions_options opts = {
.empty = empty,
+ .sign_commit = sign_commit,
};
struct replay_result result = { 0 };
struct ref_transaction *transaction = NULL;
@@ -491,6 +520,7 @@ static int cmd_history_fixup(int argc,
enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;
enum ref_action action = REF_ACTION_DEFAULT;
enum commit_tree_flags flags = 0;
+ const char *sign_commit = NULL;
int dry_run = 0;
struct option options[] = {
OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
@@ -504,6 +534,7 @@ static int cmd_history_fixup(int argc,
OPT_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)",
N_("how to handle commits that become empty"),
PARSE_OPT_NONEG, parse_opt_empty),
+ OPT_HISTORY_GPG_SIGN(&sign_commit),
OPT_END(),
};
struct merge_result merge_result = { 0 };
@@ -515,12 +546,13 @@ static int cmd_history_fixup(int argc,
bool skip_commit = false;
int ret;
+ repo_config(repo, history_config, &sign_commit);
+
argc = parse_options(argc, argv, prefix, options, usage, 0);
if (argc != 1) {
ret = error(_("command expects a single revision"));
goto out;
}
- repo_config(repo, git_default_config, NULL);
if (action == REF_ACTION_DEFAULT)
action = REF_ACTION_BRANCHES;
@@ -645,7 +677,7 @@ static int cmd_history_fixup(int argc,
if (!skip_commit) {
ret = commit_tree_ext(repo, "fixup", original, original->parents,
&original_tree->object.oid, &merge_result.tree->object.oid,
- &rewritten, flags);
+ &rewritten, sign_commit, flags);
if (ret < 0) {
ret = error(_("failed writing fixed-up commit"));
goto out;
@@ -655,7 +687,7 @@ static int cmd_history_fixup(int argc,
strbuf_addf(&reflog_msg, "fixup: updating %s", argv[0]);
ret = handle_reference_updates(&revs, action, original, rewritten,
- reflog_msg.buf, dry_run, empty);
+ reflog_msg.buf, dry_run, sign_commit, empty);
if (ret < 0) {
ret = error(_("failed replaying descendants"));
goto out;
@@ -680,6 +712,7 @@ static int cmd_history_reword(int argc,
NULL,
};
enum ref_action action = REF_ACTION_DEFAULT;
+ const char *sign_commit = NULL;
int dry_run = 0;
struct option options[] = {
OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
@@ -687,6 +720,7 @@ static int cmd_history_reword(int argc,
PARSE_OPT_NONEG, parse_ref_action),
OPT_BOOL('n', "dry-run", &dry_run,
N_("perform a dry-run without updating any refs")),
+ OPT_HISTORY_GPG_SIGN(&sign_commit),
OPT_END(),
};
struct strbuf reflog_msg = STRBUF_INIT;
@@ -694,12 +728,13 @@ static int cmd_history_reword(int argc,
struct rev_info revs = { 0 };
int ret;
+ repo_config(repo, history_config, &sign_commit);
+
argc = parse_options(argc, argv, prefix, options, usage, 0);
if (argc != 1) {
ret = error(_("command expects a single revision"));
goto out;
}
- repo_config(repo, git_default_config, NULL);
if (action == REF_ACTION_DEFAULT)
action = REF_ACTION_BRANCHES;
@@ -714,7 +749,8 @@ static int cmd_history_reword(int argc,
if (ret)
goto out;
- ret = commit_tree_with_edited_message(repo, "reworded", original, &rewritten);
+ ret = commit_tree_with_edited_message(repo, "reworded", original,
+ &rewritten, sign_commit);
if (ret < 0) {
ret = error(_("failed writing reworded commit"));
goto out;
@@ -723,7 +759,8 @@ static int cmd_history_reword(int argc,
strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
ret = handle_reference_updates(&revs, action, original, rewritten,
- reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
+ reflog_msg.buf, dry_run, sign_commit,
+ REPLAY_EMPTY_COMMIT_ABORT);
if (ret < 0) {
ret = error(_("failed replaying descendants"));
goto out;
@@ -785,7 +822,8 @@ static int write_ondisk_index(struct repository *repo,
static int split_commit(struct repository *repo,
struct commit *original,
struct pathspec *pathspec,
- struct commit **out)
+ struct commit **out,
+ const char *sign_commit)
{
struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct strbuf index_file = STRBUF_INIT;
@@ -862,7 +900,8 @@ static int split_commit(struct repository *repo,
* that shall be diffed against is the parent of the original commit.
*/
ret = commit_tree_ext(repo, "split-out", original, original->parents, &parent_tree_oid,
- &split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);
+ &split_tree->object.oid, &first_commit, sign_commit,
+ COMMIT_TREE_EDIT_MESSAGE);
if (ret < 0) {
ret = error(_("failed writing first commit"));
goto out;
@@ -879,7 +918,8 @@ static int split_commit(struct repository *repo,
new_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;
ret = commit_tree_ext(repo, "split-out", original, parents, old_tree_oid,
- new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);
+ new_tree_oid, &second_commit, sign_commit,
+ COMMIT_TREE_EDIT_MESSAGE);
if (ret < 0) {
ret = error(_("failed writing second commit"));
goto out;
@@ -907,6 +947,7 @@ static int cmd_history_split(int argc,
NULL,
};
enum ref_action action = REF_ACTION_DEFAULT;
+ const char *sign_commit = NULL;
int dry_run = 0;
struct option options[] = {
OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
@@ -914,6 +955,7 @@ static int cmd_history_split(int argc,
PARSE_OPT_NONEG, parse_ref_action),
OPT_BOOL('n', "dry-run", &dry_run,
N_("perform a dry-run without updating any refs")),
+ OPT_HISTORY_GPG_SIGN(&sign_commit),
OPT_END(),
};
struct commit *original, *rewritten = NULL;
@@ -922,12 +964,13 @@ static int cmd_history_split(int argc,
struct rev_info revs = { 0 };
int ret;
+ repo_config(repo, history_config, &sign_commit);
+
argc = parse_options(argc, argv, prefix, options, usage, 0);
if (argc < 1) {
ret = error(_("command expects a committish"));
goto out;
}
- repo_config(repo, git_default_config, NULL);
if (action == REF_ACTION_DEFAULT)
action = REF_ACTION_BRANCHES;
@@ -953,14 +996,15 @@ static int cmd_history_split(int argc,
goto out;
}
- ret = split_commit(repo, original, &pathspec, &rewritten);
+ ret = split_commit(repo, original, &pathspec, &rewritten, sign_commit);
if (ret < 0)
goto out;
strbuf_addf(&reflog_msg, "split: updating %s", argv[0]);
ret = handle_reference_updates(&revs, action, original, rewritten,
- reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
+ reflog_msg.buf, dry_run, sign_commit,
+ REPLAY_EMPTY_COMMIT_ABORT);
if (ret < 0) {
ret = error(_("failed replaying descendants"));
goto out;
diff --git a/replay.c b/replay.c
index da531d5bc6..683c384ef8 100644
--- a/replay.c
+++ b/replay.c
@@ -81,13 +81,13 @@ static struct commit *create_commit(struct repository *repo,
struct tree *tree,
struct commit *based_on,
struct commit *parent,
- enum replay_mode mode)
+ enum replay_mode mode,
+ const char *sign_commit)
{
struct object_id ret;
struct object *obj = NULL;
struct commit_list *parents = NULL;
char *author = NULL;
- char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
struct commit_extra_header *extra = NULL;
struct strbuf msg = STRBUF_INIT;
const char *out_enc = get_commit_output_encoding();
@@ -270,7 +270,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
struct merge_options *merge_opt,
struct merge_result *result,
enum replay_mode mode,
- enum replay_empty_commit_action empty)
+ enum replay_empty_commit_action empty,
+ const char *sign_commit)
{
struct commit *base, *replayed_base;
struct tree *pickme_tree, *base_tree, *replayed_base_tree;
@@ -341,7 +342,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
}
}
- return create_commit(repo, result->tree, pickme, replayed_base, mode);
+ return create_commit(repo, result->tree, pickme, replayed_base, mode,
+ sign_commit);
}
void replay_result_release(struct replay_result *result)
@@ -431,7 +433,8 @@ int replay_revisions(struct rev_info *revs,
last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
mode == REPLAY_MODE_REVERT ? last_commit : onto,
- &merge_opt, &result, mode, opts->empty);
+ &merge_opt, &result, mode, opts->empty,
+ opts->sign_commit);
if (!last_commit)
break;
diff --git a/replay.h b/replay.h
index faf95c7459..c715504d78 100644
--- a/replay.h
+++ b/replay.h
@@ -57,6 +57,12 @@ struct replay_revisions_options {
*/
int contained;
+ /*
+ * Key used to sign newly-created commits. An empty string requests the
+ * default configured signing key, and NULL disables signing.
+ */
+ const char *sign_commit;
+
/*
* Controls what to do when a replayed commit becomes empty.
* Defaults to REPLAY_EMPTY_COMMIT_DROP.
--
2.55.0
^ permalink raw reply related [flat|nested] 4+ messages in thread