[PATCH 1/3] builtin/history: sign rewritten commits
From: Souma <hidden>
Date: 2026-07-03 14:51:26
Subsystem:
the rest · Maintainer:
Linus Torvalds
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 <redacted> --- 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