{"thread":{"id":"65920","subject":"[PATCH 0/3] history: sign rewritten commits","startedAt":"2026-07-03T14:51:06Z","lastAt":"2026-10-05T06:59:42Z","messageCount":29,"participants":["Souma","Patrick Steinhardt","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":3},"messages":[{"id":"547098","messageId":"20260703145037.69832-1-git@5ouma.me","threadId":"65920","inReplyTo":null,"subject":"[PATCH 0/3] history: sign rewritten commits","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-07-03T14:50:34Z","receivedAt":"2026-07-03T14:51:06Z","isPatch":true,"body":"This series updates `git history fixup`, `reword`, and `split` to honor `commit.gpgsign` as well as the `-S/--gpg-sign` and `--no-gpg-sign` options.\nIt adds regression tests that cover configuration-driven signing, command-line overrides, and the handling of replayed descendant commits.\nFinally, it updates the history documentation to describe the new signing behavior and available options.\n\nSouma (3):\n  builtin/history: sign rewritten commits\n  doc: document history signing options\n  t345x: cover signed history rewrites\n\n Documentation/git-history.adoc | 14 ++++--\n builtin/history.c              | 80 ++++++++++++++++++++++++++--------\n replay.c                       | 13 +++---\n replay.h                       |  6 +++\n t/t3451-history-reword.sh      | 39 +++++++++++++++++\n t/t3452-history-split.sh       | 44 +++++++++++++++++++\n t/t3453-history-fixup.sh       | 39 +++++++++++++++++\n 7 files changed, 209 insertions(+), 26 deletions(-)\n\n-- \n2.55.0\n\n"},{"id":"547099","messageId":"20260703145037.69832-2-git@5ouma.me","threadId":"65920","inReplyTo":"20260703145037.69832-1-git@5ouma.me","subject":"[PATCH 1/3] builtin/history: sign rewritten commits","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-07-03T14:50:35Z","receivedAt":"2026-07-03T14:51:26Z","isPatch":true,"body":"The history commands create replacement commits directly instead of\nusing the sequencer or the commit porcelain. As a result, rewritten\ncommits ignore commit.gpgsign and cannot be signed on demand.\n\nRead the usual signing configuration before parsing history options.\nAdd the commit-style -S/--gpg-sign knob, and pass the selected\nsigning key through direct rewrites and replayed descendants.\n\nSigned-off-by: Souma <git@5ouma.me>\n---\n builtin/history.c | 80 ++++++++++++++++++++++++++++++++++++-----------\n replay.c          | 13 +++++---\n replay.h          |  6 ++++\n 3 files changed, 76 insertions(+), 23 deletions(-)\n\ndiff --git a/builtin/history.c b/builtin/history.c\nindex 091465a59e..8d669cf539 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -25,11 +25,11 @@\n #include \"wt-status.h\"\n \n #define GIT_HISTORY_FIXUP_USAGE \\\n-\tN_(\"git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]\")\n+\tN_(\"git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\")\n #define GIT_HISTORY_REWORD_USAGE \\\n-\tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\")\n+\tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]\")\n #define GIT_HISTORY_SPLIT_USAGE \\\n-\tN_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\")\n+\tN_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]\")\n \n static void change_data_free(void *util, const char *str UNUSED)\n {\n@@ -98,6 +98,30 @@ enum commit_tree_flags {\n \tCOMMIT_TREE_EDIT_MESSAGE = (1 << 0),\n };\n \n+static int history_config(const char *var, const char *value,\n+\t\t\t  const struct config_context *ctx, void *data)\n+{\n+\tconst char **sign_commit = data;\n+\n+\tif (!strcmp(var, \"commit.gpgsign\")) {\n+\t\t*sign_commit = git_config_bool(var, value) ? \"\" : NULL;\n+\t\treturn 0;\n+\t}\n+\n+\treturn git_default_config(var, value, ctx, data);\n+}\n+\n+#define OPT_HISTORY_GPG_SIGN(v) { \\\n+\t.type = OPTION_STRING, \\\n+\t.short_name = 'S', \\\n+\t.long_name = \"gpg-sign\", \\\n+\t.value = (v), \\\n+\t.argh = N_(\"key-id\"), \\\n+\t.help = N_(\"GPG-sign rewritten commits\"), \\\n+\t.flags = PARSE_OPT_OPTARG, \\\n+\t.defval = (intptr_t) \"\", \\\n+}\n+\n static int commit_tree_ext(struct repository *repo,\n \t\t\t   const char *action,\n \t\t\t   struct commit *commit_with_message,\n@@ -105,6 +129,7 @@ static int commit_tree_ext(struct repository *repo,\n \t\t\t   const struct object_id *old_tree,\n \t\t\t   const struct object_id *new_tree,\n \t\t\t   struct commit **out,\n+\t\t\t   const char *sign_commit,\n \t\t\t   enum commit_tree_flags flags)\n {\n \tconst char *exclude_gpgsig[] = {\n@@ -144,7 +169,7 @@ static int commit_tree_ext(struct repository *repo,\n \n \tret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree,\n \t\t\t\t   parents, &rewritten_commit_oid, original_author,\n-\t\t\t\t   NULL, NULL, original_extra_headers);\n+\t\t\t\t   NULL, sign_commit, original_extra_headers);\n \tif (ret < 0)\n \t\tgoto out;\n \n@@ -160,7 +185,8 @@ static int commit_tree_ext(struct repository *repo,\n static int commit_tree_with_edited_message(struct repository *repo,\n \t\t\t\t\t   const char *action,\n \t\t\t\t\t   struct commit *original,\n-\t\t\t\t\t   struct commit **out)\n+\t\t\t\t\t   struct commit **out,\n+\t\t\t\t\t   const char *sign_commit)\n {\n \tstruct object_id parent_tree_oid;\n \tconst struct object_id *tree_oid;\n@@ -181,7 +207,8 @@ static int commit_tree_with_edited_message(struct repository *repo,\n \t}\n \n \treturn commit_tree_ext(repo, action, original, original->parents,\n-\t\t\t       &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);\n+\t\t\t       &parent_tree_oid, tree_oid, out, sign_commit,\n+\t\t\t       COMMIT_TREE_EDIT_MESSAGE);\n }\n \n enum ref_action {\n@@ -339,11 +366,13 @@ static int handle_reference_updates(struct rev_info *revs,\n \t\t\t\t    struct commit *rewritten,\n \t\t\t\t    const char *reflog_msg,\n \t\t\t\t    int dry_run,\n+\t\t\t\t    const char *sign_commit,\n \t\t\t\t    enum replay_empty_commit_action empty)\n {\n \tconst struct name_decoration *decoration;\n \tstruct replay_revisions_options opts = {\n \t\t.empty = empty,\n+\t\t.sign_commit = sign_commit,\n \t};\n \tstruct replay_result result = { 0 };\n \tstruct ref_transaction *transaction = NULL;\n@@ -491,6 +520,7 @@ static int cmd_history_fixup(int argc,\n \tenum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;\n \tenum ref_action action = REF_ACTION_DEFAULT;\n \tenum commit_tree_flags flags = 0;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -504,6 +534,7 @@ static int cmd_history_fixup(int argc,\n \t\tOPT_CALLBACK_F(0, \"empty\", &empty, \"(drop|keep|abort)\",\n \t\t\t       N_(\"how to handle commits that become empty\"),\n \t\t\t       PARSE_OPT_NONEG, parse_opt_empty),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct merge_result merge_result = { 0 };\n@@ -515,12 +546,13 @@ static int cmd_history_fixup(int argc,\n \tbool skip_commit = false;\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n+\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc != 1) {\n \t\tret = error(_(\"command expects a single revision\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -645,7 +677,7 @@ static int cmd_history_fixup(int argc,\n \tif (!skip_commit) {\n \t\tret = commit_tree_ext(repo, \"fixup\", original, original->parents,\n \t\t\t\t      &original_tree->object.oid, &merge_result.tree->object.oid,\n-\t\t\t\t      &rewritten, flags);\n+\t\t\t\t      &rewritten, sign_commit, flags);\n \t\tif (ret < 0) {\n \t\t\tret = error(_(\"failed writing fixed-up commit\"));\n \t\t\tgoto out;\n@@ -655,7 +687,7 @@ static int cmd_history_fixup(int argc,\n \tstrbuf_addf(&reflog_msg, \"fixup: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf, dry_run, empty);\n+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit, empty);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\n@@ -680,6 +712,7 @@ static int cmd_history_reword(int argc,\n \t\tNULL,\n \t};\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -687,6 +720,7 @@ static int cmd_history_reword(int argc,\n \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n \t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n \t\t\t N_(\"perform a dry-run without updating any refs\")),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n@@ -694,12 +728,13 @@ static int cmd_history_reword(int argc,\n \tstruct rev_info revs = { 0 };\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n+\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc != 1) {\n \t\tret = error(_(\"command expects a single revision\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -714,7 +749,8 @@ static int cmd_history_reword(int argc,\n \tif (ret)\n \t\tgoto out;\n \n-\tret = commit_tree_with_edited_message(repo, \"reworded\", original, &rewritten);\n+\tret = commit_tree_with_edited_message(repo, \"reworded\", original,\n+\t\t\t\t\t      &rewritten, sign_commit);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing reworded commit\"));\n \t\tgoto out;\n@@ -723,7 +759,8 @@ static int cmd_history_reword(int argc,\n \tstrbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);\n+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit,\n+\t\t\t\t       REPLAY_EMPTY_COMMIT_ABORT);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\n@@ -785,7 +822,8 @@ static int write_ondisk_index(struct repository *repo,\n static int split_commit(struct repository *repo,\n \t\t\tstruct commit *original,\n \t\t\tstruct pathspec *pathspec,\n-\t\t\tstruct commit **out)\n+\t\t\tstruct commit **out,\n+\t\t\tconst char *sign_commit)\n {\n \tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n \tstruct strbuf index_file = STRBUF_INIT;\n@@ -862,7 +900,8 @@ static int split_commit(struct repository *repo,\n \t * that shall be diffed against is the parent of the original commit.\n \t */\n \tret = commit_tree_ext(repo, \"split-out\", original, original->parents, &parent_tree_oid,\n-\t\t\t      &split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);\n+\t\t\t      &split_tree->object.oid, &first_commit, sign_commit,\n+\t\t\t      COMMIT_TREE_EDIT_MESSAGE);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing first commit\"));\n \t\tgoto out;\n@@ -879,7 +918,8 @@ static int split_commit(struct repository *repo,\n \tnew_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;\n \n \tret = commit_tree_ext(repo, \"split-out\", original, parents, old_tree_oid,\n-\t\t\t      new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);\n+\t\t\t      new_tree_oid, &second_commit, sign_commit,\n+\t\t\t      COMMIT_TREE_EDIT_MESSAGE);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing second commit\"));\n \t\tgoto out;\n@@ -907,6 +947,7 @@ static int cmd_history_split(int argc,\n \t\tNULL,\n \t};\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -914,6 +955,7 @@ static int cmd_history_split(int argc,\n \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n \t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n \t\t\t N_(\"perform a dry-run without updating any refs\")),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct commit *original, *rewritten = NULL;\n@@ -922,12 +964,13 @@ static int cmd_history_split(int argc,\n \tstruct rev_info revs = { 0 };\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n+\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc < 1) {\n \t\tret = error(_(\"command expects a committish\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -953,14 +996,15 @@ static int cmd_history_split(int argc,\n \t\tgoto out;\n \t}\n \n-\tret = split_commit(repo, original, &pathspec, &rewritten);\n+\tret = split_commit(repo, original, &pathspec, &rewritten, sign_commit);\n \tif (ret < 0)\n \t\tgoto out;\n \n \tstrbuf_addf(&reflog_msg, \"split: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);\n+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit,\n+\t\t\t\t       REPLAY_EMPTY_COMMIT_ABORT);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\ndiff --git a/replay.c b/replay.c\nindex da531d5bc6..683c384ef8 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -81,13 +81,13 @@ static struct commit *create_commit(struct repository *repo,\n \t\t\t\t    struct tree *tree,\n \t\t\t\t    struct commit *based_on,\n \t\t\t\t    struct commit *parent,\n-\t\t\t\t    enum replay_mode mode)\n+\t\t\t\t    enum replay_mode mode,\n+\t\t\t\t    const char *sign_commit)\n {\n \tstruct object_id ret;\n \tstruct object *obj = NULL;\n \tstruct commit_list *parents = NULL;\n \tchar *author = NULL;\n-\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n \tstruct commit_extra_header *extra = NULL;\n \tstruct strbuf msg = STRBUF_INIT;\n \tconst char *out_enc = get_commit_output_encoding();\n@@ -270,7 +270,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \t\t\t\t\t  struct merge_options *merge_opt,\n \t\t\t\t\t  struct merge_result *result,\n \t\t\t\t\t  enum replay_mode mode,\n-\t\t\t\t\t  enum replay_empty_commit_action empty)\n+\t\t\t\t\t  enum replay_empty_commit_action empty,\n+\t\t\t\t\t  const char *sign_commit)\n {\n \tstruct commit *base, *replayed_base;\n \tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n@@ -341,7 +342,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \t\t}\n \t}\n \n-\treturn create_commit(repo, result->tree, pickme, replayed_base, mode);\n+\treturn create_commit(repo, result->tree, pickme, replayed_base, mode,\n+\t\t\t     sign_commit);\n }\n \n void replay_result_release(struct replay_result *result)\n@@ -431,7 +433,8 @@ int replay_revisions(struct rev_info *revs,\n \n \t\tlast_commit = pick_regular_commit(revs->repo, commit, replayed_commits,\n \t\t\t\t\t\t  mode == REPLAY_MODE_REVERT ? last_commit : onto,\n-\t\t\t\t\t\t  &merge_opt, &result, mode, opts->empty);\n+\t\t\t\t\t\t  &merge_opt, &result, mode, opts->empty,\n+\t\t\t\t\t\t  opts->sign_commit);\n \t\tif (!last_commit)\n \t\t\tbreak;\n \ndiff --git a/replay.h b/replay.h\nindex faf95c7459..c715504d78 100644\n--- a/replay.h\n+++ b/replay.h\n@@ -57,6 +57,12 @@ struct replay_revisions_options {\n \t */\n \tint contained;\n \n+\t/*\n+\t * Key used to sign newly-created commits. An empty string requests the\n+\t * default configured signing key, and NULL disables signing.\n+\t */\n+\tconst char *sign_commit;\n+\n \t/*\n \t * Controls what to do when a replayed commit becomes empty.\n \t * Defaults to REPLAY_EMPTY_COMMIT_DROP.\n-- \n2.55.0\n\n"},{"id":"547100","messageId":"20260703145037.69832-3-git@5ouma.me","threadId":"65920","inReplyTo":"20260703145037.69832-1-git@5ouma.me","subject":"[PATCH 2/3] doc: document history signing options","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-07-03T14:50:36Z","receivedAt":"2026-07-03T14:51:32Z","isPatch":true,"body":"The history manual and usage text should describe the signing controls now\naccepted by fixup, reword, and split.\n\nDocument -S/--gpg-sign and --no-gpg-sign with the same key-id spelling and\nconfiguration override behavior used by commit-style signing options.\n\nSigned-off-by: Souma <git@5ouma.me>\n---\n Documentation/git-history.adoc | 14 +++++++++++---\n 1 file changed, 11 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 2ba8121795..a1dd5c8580 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,9 +8,9 @@ git-history - EXPERIMENTAL: Rewrite history\n SYNOPSIS\n --------\n [synopsis]\n-git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]\n-git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\n-git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\n+git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\n+git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]\n+git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]\n \n DESCRIPTION\n -----------\n@@ -109,6 +109,14 @@ OPTIONS\n `--reedit-message`::\n \tOpen an editor to modify the target commit's message.\n \n+`-S[<key-id>]`::\n+`--gpg-sign[=<key-id>]`::\n+`--no-gpg-sign`::\n+\tGPG-sign rewritten commits. The _<key-id>_ argument is optional and\n+\tdefaults to the committer identity; if specified, it must be stuck to\n+\tthe option without a space. `--no-gpg-sign` is useful to countermand\n+\tboth `commit.gpgSign` configuration and earlier `--gpg-sign`.\n+\n `--empty=(drop|keep|abort)`::\n \tControl what happens when a commit becomes empty as a result of the\n \tfixup. This can happen in two situations:\n-- \n2.55.0\n\n"},{"id":"547101","messageId":"20260703145037.69832-4-git@5ouma.me","threadId":"65920","inReplyTo":"20260703145037.69832-1-git@5ouma.me","subject":"[PATCH 3/3] t345x: cover signed history rewrites","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-07-03T14:50:37Z","receivedAt":"2026-07-03T14:51:36Z","isPatch":true,"body":"History signing needs regression coverage because these commands bypass the\nusual commit machinery and create replacement commits through lower-level\nAPIs.\n\nAdd GPG-gated tests for config-driven signing, command-line signing,\n--no-gpg-sign precedence, and signing of replayed descendants after fixup,\nreword, and split.\n\nSigned-off-by: Souma <git@5ouma.me>\n---\n t/t3451-history-reword.sh | 39 ++++++++++++++++++++++++++++++++++\n t/t3452-history-split.sh  | 44 +++++++++++++++++++++++++++++++++++++++\n t/t3453-history-fixup.sh  | 39 ++++++++++++++++++++++++++++++++++\n 3 files changed, 122 insertions(+)\n\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex de7b357685..5b41fb6489 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -4,6 +4,7 @@ test_description='tests for git-history reword subcommand'\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY/lib-log-graph.sh\"\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n reword_with_message () {\n \tcat >message &&\n@@ -26,6 +27,37 @@ expect_log () {\n \ttest_cmp expect actual\n }\n \n+test_reword_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"reword $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit first &&\n+\t\t\ttest_commit second &&\n+\t\t\ttest_commit third &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\treword_with_message $* HEAD~ <<-EOF &&\n+\t\t\tsecond reworded\n+\t\t\tEOF\n+\n+\t\t\t$must_fail git verify-commit HEAD~ &&\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'can reword tip of a branch' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -77,6 +109,13 @@ test_expect_success 'can reword commit in the middle' '\n \t)\n '\n \n+test_reword_gpg_sign ! false\n+test_reword_gpg_sign   true\n+test_reword_gpg_sign   false --gpg-sign\n+test_reword_gpg_sign ! true  --no-gpg-sign\n+test_reword_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_reword_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n test_expect_success 'can reword commit in the middle even on detached head' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\ndiff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh\nindex 8ed0cebb50..e96f492cc6 100755\n--- a/t/t3452-history-split.sh\n+++ b/t/t3452-history-split.sh\n@@ -4,6 +4,7 @@ test_description='tests for git-history split subcommand'\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY/lib-log-graph.sh\"\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n # The fake editor takes multiple arguments, each of which represents a commit\n # message. Subsequent invocations of the editor will then yield those messages\n@@ -36,6 +37,42 @@ expect_tree_entries () {\n \ttest_cmp expect actual\n }\n \n+test_split_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"split $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit initial &&\n+\t\t\ttouch bar foo &&\n+\t\t\tgit add . &&\n+\t\t\tgit commit -m split-me &&\n+\t\t\ttest_commit tip &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\tset_fake_editor 'first' 'second' &&\n+\t\t\tgit history split $* HEAD~ <<-EOF &&\n+\t\t\ty\n+\t\t\tn\n+\t\t\tEOF\n+\n+\t\t\t$must_fail git verify-commit HEAD~2 &&\n+\t\t\t$must_fail git verify-commit HEAD~ &&\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'refuses to work with merge commits' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -141,6 +178,13 @@ test_expect_success 'can split up tip commit' '\n \t)\n '\n \n+test_split_gpg_sign ! false\n+test_split_gpg_sign   true\n+test_split_gpg_sign   false --gpg-sign\n+test_split_gpg_sign ! true  --no-gpg-sign\n+test_split_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_split_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n test_expect_success 'can split up root commit' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\ndiff --git a/t/t3453-history-fixup.sh b/t/t3453-history-fixup.sh\nindex 868298e248..cd20a23115 100755\n--- a/t/t3453-history-fixup.sh\n+++ b/t/t3453-history-fixup.sh\n@@ -3,6 +3,7 @@\n test_description='tests for git-history fixup subcommand'\n \n . ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n fixup_with_message () {\n \tcat >message &&\n@@ -21,6 +22,37 @@ expect_changes () {\n \ttest_cmp expect actual\n }\n \n+test_fixup_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"fixup $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit first &&\n+\t\t\ttest_commit second &&\n+\t\t\ttest_commit third &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\techo fix >>second.t &&\n+\t\t\tgit add second.t &&\n+\t\t\tgit history fixup $* HEAD~ &&\n+\n+\t\t\t$must_fail git verify-commit HEAD~ &&\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'errors on missing commit argument' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -229,6 +261,13 @@ test_expect_success 'preserves commit message and authorship' '\n \t)\n '\n \n+test_fixup_gpg_sign ! false\n+test_fixup_gpg_sign   true\n+test_fixup_gpg_sign   false --gpg-sign\n+test_fixup_gpg_sign ! true  --no-gpg-sign\n+test_fixup_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_fixup_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n test_expect_success 'updates all descendant branches by default' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo --initial-branch=main &&\n-- \n2.55.0\n\n"},{"id":"548394","messageId":"aliv2_SfQ2_jh-k2@pks.im","threadId":"65920","inReplyTo":"20260703145037.69832-3-git@5ouma.me","subject":"Re: [PATCH 2/3] doc: document history signing options","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-16T10:18:03Z","receivedAt":"2026-07-16T10:18:08Z","isPatch":true,"body":"On Fri, Jul 03, 2026 at 11:50:36PM +0900, Souma wrote:\n> The history manual and usage text should describe the signing controls now\n> accepted by fixup, reword, and split.\n> \n> Document -S/--gpg-sign and --no-gpg-sign with the same key-id spelling and\n> configuration override behavior used by commit-style signing options.\n> \n> Signed-off-by: Souma <git@5ouma.me>\n> ---\n>  Documentation/git-history.adoc | 14 +++++++++++---\n>  1 file changed, 11 insertions(+), 3 deletions(-)\n\nI think this and the next commit can easily be merged into the first\none. They really belong together, and even worse t0450 probably breaks\nwith the first commit, only, as the change to the synopsis in our docs\nand in the command itself is split up across two commits.\n\nPatrick\n"},{"id":"548395","messageId":"aliv3zgfDvY3JoB9@pks.im","threadId":"65920","inReplyTo":"20260703145037.69832-2-git@5ouma.me","subject":"Re: [PATCH 1/3] builtin/history: sign rewritten commits","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-07-16T10:18:07Z","receivedAt":"2026-07-16T10:18:13Z","isPatch":true,"body":"On Fri, Jul 03, 2026 at 11:50:35PM +0900, Souma wrote:\n> diff --git a/builtin/history.c b/builtin/history.c\n> index 091465a59e..8d669cf539 100644\n> --- a/builtin/history.c\n> +++ b/builtin/history.c\n> @@ -98,6 +98,30 @@ enum commit_tree_flags {\n>  \tCOMMIT_TREE_EDIT_MESSAGE = (1 << 0),\n>  };\n>  \n> +static int history_config(const char *var, const char *value,\n> +\t\t\t  const struct config_context *ctx, void *data)\n> +{\n> +\tconst char **sign_commit = data;\n> +\n> +\tif (!strcmp(var, \"commit.gpgsign\")) {\n> +\t\t*sign_commit = git_config_bool(var, value) ? \"\" : NULL;\n> +\t\treturn 0;\n> +\t}\n> +\n> +\treturn git_default_config(var, value, ctx, data);\n\nShouldn't we rather pass `NULL` instead of `data`? It works, sure, but\nonly because `git_default_config()` doesn't use `data` at all.\n\n> @@ -160,7 +185,8 @@ static int commit_tree_ext(struct repository *repo,\n>  static int commit_tree_with_edited_message(struct repository *repo,\n>  \t\t\t\t\t   const char *action,\n>  \t\t\t\t\t   struct commit *original,\n> -\t\t\t\t\t   struct commit **out)\n> +\t\t\t\t\t   struct commit **out,\n> +\t\t\t\t\t   const char *sign_commit)\n\nNit: the `out` parameter should continue to be the last one.\n\n> @@ -515,12 +546,13 @@ static int cmd_history_fixup(int argc,\n>  \tbool skip_commit = false;\n>  \tint ret;\n>  \n> +\trepo_config(repo, history_config, &sign_commit);\n> +\n>  \targc = parse_options(argc, argv, prefix, options, usage, 0);\n>  \tif (argc != 1) {\n>  \t\tret = error(_(\"command expects a single revision\"));\n>  \t\tgoto out;\n>  \t}\n> -\trepo_config(repo, git_default_config, NULL);\n>  \n>  \tif (action == REF_ACTION_DEFAULT)\n>  \t\taction = REF_ACTION_BRANCHES;\n\nIt might make sense to document in the commit message why we have to\nchange the order. I guess it's because of precedence, but not everyone\nmight realize that immediately.\n\n> @@ -785,7 +822,8 @@ static int write_ondisk_index(struct repository *repo,\n>  static int split_commit(struct repository *repo,\n>  \t\t\tstruct commit *original,\n>  \t\t\tstruct pathspec *pathspec,\n> -\t\t\tstruct commit **out)\n> +\t\t\tstruct commit **out,\n> +\t\t\tconst char *sign_commit)\n>  {\n>  \tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n>  \tstruct strbuf index_file = STRBUF_INIT;\n\nLikewise, let's ensure that the `out` parameter remains last.\n\n> diff --git a/replay.c b/replay.c\n> index da531d5bc6..683c384ef8 100644\n> --- a/replay.c\n> +++ b/replay.c\n\nIt might make sense to split out the changes to \"replay.c\" into a\npreparatory commit.\n\nOne interesting question is whether it really makes sense to sign _all_\ncommits. It's rather likely that the history will contain commits that\naren't even owned by you, so signing them with your signature might be a\nbit of a weird choice. I guess that might be okay-ish, but it's\ncertainly something that's worth a discussion as part of the commit\nmessage.\n\nThanks!\n\nPatrick\n"},{"id":"548513","messageId":"20260717145142.39478-1-git@5ouma.me","threadId":"65920","inReplyTo":"20260703145037.69832-1-git@5ouma.me","subject":"[PATCH v2 0/2] history: support signing rewritten commits","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-07-17T14:51:40Z","receivedAt":"2026-07-17T14:52:01Z","isPatch":true,"body":"The history commands create commits directly and via the replay\nmachinery, but currently have no way to honor `commit.gpgSign` or an\nexplicit signing request. This means users who require signed commits\nlose that property when rewriting history.\n\nTeach the replay API to accept a signing key, then expose the standard\n`-S`/`--gpg-sign[=<key-id>]` and `--no-gpg-sign` interface across the\n`history drop`, `history fixup`, `history reword`, and `history split`\nsubcommands. The selected policy applies to every new commit, including\nboth halves of a split and replayed descendants. A drop of the tip\ncreates no replacement commit and therefore has nothing to sign.\n\nThe implementation follows the precedence used by rebase, cherry-pick,\nand revert: `commit.gpgSign` supplies the default, command-line options\noverride it, and the last command-line option wins.\n\nThe signature records the attestation of the current committer to the\nrewritten commit while retaining the original author identity; it does\nnot claim authorship of commits written by somebody else.\n\nChanges since v1:\n\n - Split the replay signing plumbing into a preparatory patch\n - Fold the documentation and tests into the feature patch so each\n   commit builds and passes t0450\n - Move `sign_commit` before the output parameter of `commit_tree_ext()` and\n   update its callers accordingly\n - Pass `NULL` to `git_default_config()`\n - Document why configuration is read before command-line options\n - Clarify that every rewritten commit is signed, including commits with\n   a different author\n - Add signing support and tests for the new `history drop` subcommand\n - Add coverage for selecting an explicit signing key\n\nSouma (2):\n  replay: allow callers to sign commits\n  builtin/history: sign rewritten commits\n\n Documentation/git-history.adoc | 16 +++++--\n builtin/history.c              | 84 ++++++++++++++++++++++++++--------\n replay.c                       | 13 ++++--\n replay.h                       |  6 +++\n t/t3451-history-reword.sh      | 63 +++++++++++++++++++++++++\n t/t3452-history-split.sh       | 44 ++++++++++++++++++\n t/t3453-history-fixup.sh       | 39 ++++++++++++++++\n t/t3454-history-drop.sh        | 50 ++++++++++++++++++++\n 8 files changed, 286 insertions(+), 29 deletions(-)\n\nRange-diff against v1:\n1:  60f7c13514 ! 1:  3f4dc0b982 builtin/history: sign rewritten commits\n    @@ Metadata\n     Author: Souma <git@5ouma.me>\n\n      ## Commit message ##\n    -    builtin/history: sign rewritten commits\n    +    replay: allow callers to sign commits\n\n    -    The history commands create replacement commits directly instead of\n    -    using the sequencer or the commit porcelain. As a result, rewritten\n    -    commits ignore commit.gpgsign and cannot be signed on demand.\n    +    The replay machinery creates commits directly through\n    +    `commit_tree_extended()`, but callers cannot currently request\n    +    signatures. Commands that replay rewritten history consequently cannot\n    +    carry their signing policy through to descendant commits.\n\n    -    Read the usual signing configuration before parsing history options.\n    -    Add the commit-style -S/--gpg-sign knob, and pass the selected\n    -    signing key through direct rewrites and replayed descendants.\n    +    Add `sign_commit` to `replay_revisions_options` and thread it through\n    +    commit creation. `NULL` preserves the existing unsigned behavior, an\n    +    empty string selects the default signing key, and a non-empty string\n    +    selects an explicit key. Existing callers zero-initialize the options\n    +    structure, so their behavior is unchanged.\n\n         Signed-off-by: Souma <git@5ouma.me>\n\n    - ## builtin/history.c ##\n    -@@\n    - #include \"wt-status.h\"\n    -\n    - #define GIT_HISTORY_FIXUP_USAGE \\\n    --\tN_(\"git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]\")\n    -+\tN_(\"git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\")\n    - #define GIT_HISTORY_REWORD_USAGE \\\n    --\tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\")\n    -+\tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]\")\n    - #define GIT_HISTORY_SPLIT_USAGE \\\n    --\tN_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\")\n    -+\tN_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]\")\n    -\n    - static void change_data_free(void *util, const char *str UNUSED)\n    - {\n    -@@ builtin/history.c: enum commit_tree_flags {\n    - \tCOMMIT_TREE_EDIT_MESSAGE = (1 << 0),\n    - };\n    -\n    -+static int history_config(const char *var, const char *value,\n    -+\t\t\t  const struct config_context *ctx, void *data)\n    -+{\n    -+\tconst char **sign_commit = data;\n    -+\n    -+\tif (!strcmp(var, \"commit.gpgsign\")) {\n    -+\t\t*sign_commit = git_config_bool(var, value) ? \"\" : NULL;\n    -+\t\treturn 0;\n    -+\t}\n    -+\n    -+\treturn git_default_config(var, value, ctx, data);\n    -+}\n    -+\n    -+#define OPT_HISTORY_GPG_SIGN(v) { \\\n    -+\t.type = OPTION_STRING, \\\n    -+\t.short_name = 'S', \\\n    -+\t.long_name = \"gpg-sign\", \\\n    -+\t.value = (v), \\\n    -+\t.argh = N_(\"key-id\"), \\\n    -+\t.help = N_(\"GPG-sign rewritten commits\"), \\\n    -+\t.flags = PARSE_OPT_OPTARG, \\\n    -+\t.defval = (intptr_t) \"\", \\\n    -+}\n    -+\n    - static int commit_tree_ext(struct repository *repo,\n    - \t\t\t   const char *action,\n    - \t\t\t   struct commit *commit_with_message,\n    -@@ builtin/history.c: static int commit_tree_ext(struct repository *repo,\n    - \t\t\t   const struct object_id *old_tree,\n    - \t\t\t   const struct object_id *new_tree,\n    - \t\t\t   struct commit **out,\n    -+\t\t\t   const char *sign_commit,\n    - \t\t\t   enum commit_tree_flags flags)\n    - {\n    - \tconst char *exclude_gpgsig[] = {\n    -@@ builtin/history.c: static int commit_tree_ext(struct repository *repo,\n    -\n    - \tret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree,\n    - \t\t\t\t   parents, &rewritten_commit_oid, original_author,\n    --\t\t\t\t   NULL, NULL, original_extra_headers);\n    -+\t\t\t\t   NULL, sign_commit, original_extra_headers);\n    - \tif (ret < 0)\n    - \t\tgoto out;\n    -\n    -@@ builtin/history.c: static int commit_tree_ext(struct repository *repo,\n    - static int commit_tree_with_edited_message(struct repository *repo,\n    - \t\t\t\t\t   const char *action,\n    - \t\t\t\t\t   struct commit *original,\n    --\t\t\t\t\t   struct commit **out)\n    -+\t\t\t\t\t   struct commit **out,\n    -+\t\t\t\t\t   const char *sign_commit)\n    - {\n    - \tstruct object_id parent_tree_oid;\n    - \tconst struct object_id *tree_oid;\n    -@@ builtin/history.c: static int commit_tree_with_edited_message(struct repository *repo,\n    - \t}\n    -\n    - \treturn commit_tree_ext(repo, action, original, original->parents,\n    --\t\t\t       &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);\n    -+\t\t\t       &parent_tree_oid, tree_oid, out, sign_commit,\n    -+\t\t\t       COMMIT_TREE_EDIT_MESSAGE);\n    - }\n    -\n    - enum ref_action {\n    -@@ builtin/history.c: static int handle_reference_updates(struct rev_info *revs,\n    - \t\t\t\t    struct commit *rewritten,\n    - \t\t\t\t    const char *reflog_msg,\n    - \t\t\t\t    int dry_run,\n    -+\t\t\t\t    const char *sign_commit,\n    - \t\t\t\t    enum replay_empty_commit_action empty)\n    - {\n    - \tconst struct name_decoration *decoration;\n    - \tstruct replay_revisions_options opts = {\n    - \t\t.empty = empty,\n    -+\t\t.sign_commit = sign_commit,\n    - \t};\n    - \tstruct replay_result result = { 0 };\n    - \tstruct ref_transaction *transaction = NULL;\n    -@@ builtin/history.c: static int cmd_history_fixup(int argc,\n    - \tenum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;\n    - \tenum ref_action action = REF_ACTION_DEFAULT;\n    - \tenum commit_tree_flags flags = 0;\n    -+\tconst char *sign_commit = NULL;\n    - \tint dry_run = 0;\n    - \tstruct option options[] = {\n    - \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n    -@@ builtin/history.c: static int cmd_history_fixup(int argc,\n    - \t\tOPT_CALLBACK_F(0, \"empty\", &empty, \"(drop|keep|abort)\",\n    - \t\t\t       N_(\"how to handle commits that become empty\"),\n    - \t\t\t       PARSE_OPT_NONEG, parse_opt_empty),\n    -+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n    - \t\tOPT_END(),\n    - \t};\n    - \tstruct merge_result merge_result = { 0 };\n    -@@ builtin/history.c: static int cmd_history_fixup(int argc,\n    - \tbool skip_commit = false;\n    - \tint ret;\n    -\n    -+\trepo_config(repo, history_config, &sign_commit);\n    -+\n    - \targc = parse_options(argc, argv, prefix, options, usage, 0);\n    - \tif (argc != 1) {\n    - \t\tret = error(_(\"command expects a single revision\"));\n    - \t\tgoto out;\n    - \t}\n    --\trepo_config(repo, git_default_config, NULL);\n    -\n    - \tif (action == REF_ACTION_DEFAULT)\n    - \t\taction = REF_ACTION_BRANCHES;\n    -@@ builtin/history.c: static int cmd_history_fixup(int argc,\n    - \tif (!skip_commit) {\n    - \t\tret = commit_tree_ext(repo, \"fixup\", original, original->parents,\n    - \t\t\t\t      &original_tree->object.oid, &merge_result.tree->object.oid,\n    --\t\t\t\t      &rewritten, flags);\n    -+\t\t\t\t      &rewritten, sign_commit, flags);\n    - \t\tif (ret < 0) {\n    - \t\t\tret = error(_(\"failed writing fixed-up commit\"));\n    - \t\t\tgoto out;\n    -@@ builtin/history.c: static int cmd_history_fixup(int argc,\n    - \tstrbuf_addf(&reflog_msg, \"fixup: updating %s\", argv[0]);\n    -\n    - \tret = handle_reference_updates(&revs, action, original, rewritten,\n    --\t\t\t\t       reflog_msg.buf, dry_run, empty);\n    -+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit, empty);\n    - \tif (ret < 0) {\n    - \t\tret = error(_(\"failed replaying descendants\"));\n    - \t\tgoto out;\n    -@@ builtin/history.c: static int cmd_history_reword(int argc,\n    - \t\tNULL,\n    - \t};\n    - \tenum ref_action action = REF_ACTION_DEFAULT;\n    -+\tconst char *sign_commit = NULL;\n    - \tint dry_run = 0;\n    - \tstruct option options[] = {\n    - \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n    -@@ builtin/history.c: static int cmd_history_reword(int argc,\n    - \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n    - \t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n    - \t\t\t N_(\"perform a dry-run without updating any refs\")),\n    -+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n    - \t\tOPT_END(),\n    - \t};\n    - \tstruct strbuf reflog_msg = STRBUF_INIT;\n    -@@ builtin/history.c: static int cmd_history_reword(int argc,\n    - \tstruct rev_info revs = { 0 };\n    - \tint ret;\n    -\n    -+\trepo_config(repo, history_config, &sign_commit);\n    -+\n    - \targc = parse_options(argc, argv, prefix, options, usage, 0);\n    - \tif (argc != 1) {\n    - \t\tret = error(_(\"command expects a single revision\"));\n    - \t\tgoto out;\n    - \t}\n    --\trepo_config(repo, git_default_config, NULL);\n    -\n    - \tif (action == REF_ACTION_DEFAULT)\n    - \t\taction = REF_ACTION_BRANCHES;\n    -@@ builtin/history.c: static int cmd_history_reword(int argc,\n    - \tif (ret)\n    - \t\tgoto out;\n    -\n    --\tret = commit_tree_with_edited_message(repo, \"reworded\", original, &rewritten);\n    -+\tret = commit_tree_with_edited_message(repo, \"reworded\", original,\n    -+\t\t\t\t\t      &rewritten, sign_commit);\n    - \tif (ret < 0) {\n    - \t\tret = error(_(\"failed writing reworded commit\"));\n    - \t\tgoto out;\n    -@@ builtin/history.c: static int cmd_history_reword(int argc,\n    - \tstrbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n    -\n    - \tret = handle_reference_updates(&revs, action, original, rewritten,\n    --\t\t\t\t       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);\n    -+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit,\n    -+\t\t\t\t       REPLAY_EMPTY_COMMIT_ABORT);\n    - \tif (ret < 0) {\n    - \t\tret = error(_(\"failed replaying descendants\"));\n    - \t\tgoto out;\n    -@@ builtin/history.c: static int write_ondisk_index(struct repository *repo,\n    - static int split_commit(struct repository *repo,\n    - \t\t\tstruct commit *original,\n    - \t\t\tstruct pathspec *pathspec,\n    --\t\t\tstruct commit **out)\n    -+\t\t\tstruct commit **out,\n    -+\t\t\tconst char *sign_commit)\n    - {\n    - \tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n    - \tstruct strbuf index_file = STRBUF_INIT;\n    -@@ builtin/history.c: static int split_commit(struct repository *repo,\n    - \t * that shall be diffed against is the parent of the original commit.\n    - \t */\n    - \tret = commit_tree_ext(repo, \"split-out\", original, original->parents, &parent_tree_oid,\n    --\t\t\t      &split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);\n    -+\t\t\t      &split_tree->object.oid, &first_commit, sign_commit,\n    -+\t\t\t      COMMIT_TREE_EDIT_MESSAGE);\n    - \tif (ret < 0) {\n    - \t\tret = error(_(\"failed writing first commit\"));\n    - \t\tgoto out;\n    -@@ builtin/history.c: static int split_commit(struct repository *repo,\n    - \tnew_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;\n    -\n    - \tret = commit_tree_ext(repo, \"split-out\", original, parents, old_tree_oid,\n    --\t\t\t      new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);\n    -+\t\t\t      new_tree_oid, &second_commit, sign_commit,\n    -+\t\t\t      COMMIT_TREE_EDIT_MESSAGE);\n    - \tif (ret < 0) {\n    - \t\tret = error(_(\"failed writing second commit\"));\n    - \t\tgoto out;\n    -@@ builtin/history.c: static int cmd_history_split(int argc,\n    - \t\tNULL,\n    - \t};\n    - \tenum ref_action action = REF_ACTION_DEFAULT;\n    -+\tconst char *sign_commit = NULL;\n    - \tint dry_run = 0;\n    - \tstruct option options[] = {\n    - \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n    -@@ builtin/history.c: static int cmd_history_split(int argc,\n    - \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n    - \t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n    - \t\t\t N_(\"perform a dry-run without updating any refs\")),\n    -+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n    - \t\tOPT_END(),\n    - \t};\n    - \tstruct commit *original, *rewritten = NULL;\n    -@@ builtin/history.c: static int cmd_history_split(int argc,\n    - \tstruct rev_info revs = { 0 };\n    - \tint ret;\n    -\n    -+\trepo_config(repo, history_config, &sign_commit);\n    -+\n    - \targc = parse_options(argc, argv, prefix, options, usage, 0);\n    - \tif (argc < 1) {\n    - \t\tret = error(_(\"command expects a committish\"));\n    - \t\tgoto out;\n    - \t}\n    --\trepo_config(repo, git_default_config, NULL);\n    -\n    - \tif (action == REF_ACTION_DEFAULT)\n    - \t\taction = REF_ACTION_BRANCHES;\n    -@@ builtin/history.c: static int cmd_history_split(int argc,\n    - \t\tgoto out;\n    - \t}\n    -\n    --\tret = split_commit(repo, original, &pathspec, &rewritten);\n    -+\tret = split_commit(repo, original, &pathspec, &rewritten, sign_commit);\n    - \tif (ret < 0)\n    - \t\tgoto out;\n    -\n    - \tstrbuf_addf(&reflog_msg, \"split: updating %s\", argv[0]);\n    -\n    - \tret = handle_reference_updates(&revs, action, original, rewritten,\n    --\t\t\t\t       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);\n    -+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit,\n    -+\t\t\t\t       REPLAY_EMPTY_COMMIT_ABORT);\n    - \tif (ret < 0) {\n    - \t\tret = error(_(\"failed replaying descendants\"));\n    - \t\tgoto out;\n    -\n      ## replay.c ##\n     @@ replay.c: static struct commit *create_commit(struct repository *repo,\n      \t\t\t\t    struct tree *tree,\n2:  9935928b01 < -:  ---------- doc: document history signing options\n3:  c017e90034 ! 2:  0e63c0b66a t345x: cover signed history rewrites\n    @@ Metadata\n     Author: Souma <git@5ouma.me>\n\n      ## Commit message ##\n    -    t345x: cover signed history rewrites\n    +    builtin/history: sign rewritten commits\n\n    -    History signing needs regression coverage because these commands bypass the\n    -    usual commit machinery and create replacement commits through lower-level\n    -    APIs.\n    +    The history commands create replacement commits directly instead of\n    +    using the sequencer or the commit porcelain. As a result, rewritten\n    +    commits ignore `commit.gpgSign` and cannot be signed on demand.\n\n    -    Add GPG-gated tests for config-driven signing, command-line signing,\n    -    --no-gpg-sign precedence, and signing of replayed descendants after fixup,\n    -    reword, and split.\n    +    Read the signing configuration before parsing options so that it\n    +    establishes the default and later `-S`/`--gpg-sign` or `--no-gpg-sign`\n    +    options override it. Pass the selected key through direct rewrites and\n    +    the replay machinery.\n    +\n    +    Sign every newly created commit, including both halves of a split and\n    +    replayed descendants. Dropping the tip creates no replacement commit,\n    +    so there is nothing to sign. As with `rebase --gpg-sign`, the signature\n    +    records the attestation of the current committer to the rewritten\n    +    commit while retaining the original author identity; it does not claim\n    +    authorship of commits written by somebody else.\n    +\n    +    Document the behavior and add GPG-gated coverage for configuration,\n    +    command-line overrides, last-option-wins precedence, replayed\n    +    descendants, split commits, an explicit signing key, and the\n    +    no-new-commit drop case.\n\n         Signed-off-by: Souma <git@5ouma.me>\n\n    + ## Documentation/git-history.adoc ##\n    +@@ Documentation/git-history.adoc: git-history - EXPERIMENTAL: Rewrite history\n    + SYNOPSIS\n    + --------\n    + [synopsis]\n    +-git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]\n    +-git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]\n    +-git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\n    +-git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\n    ++git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\n    ++git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\n    ++git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]\n    ++git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]\n    +\n    + DESCRIPTION\n    + -----------\n    +@@ Documentation/git-history.adoc: OPTIONS\n    + `--reedit-message`::\n    + \tOpen an editor to modify the target commit's message.\n    +\n    ++`-S[<key-id>]`::\n    ++`--gpg-sign[=<key-id>]`::\n    ++`--no-gpg-sign`::\n    ++\tGPG-sign rewritten commits. The _<key-id>_ argument is optional and\n    ++\tdefaults to the committer identity; if specified, it must be stuck to\n    ++\tthe option without a space. `--no-gpg-sign` is useful to countermand\n    ++\tboth `commit.gpgSign` configuration and earlier `--gpg-sign`.\n    ++\n    + `--empty=(drop|keep|abort)`::\n    + \tControl what happens when a commit becomes empty as a result of the\n    + \tfixup. This can happen in two situations:\n    +\n    + ## builtin/history.c ##\n    +@@\n    + #include \"wt-status.h\"\n    +\n    + #define GIT_HISTORY_DROP_USAGE \\\n    +-\tN_(\"git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]\")\n    ++\tN_(\"git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\")\n    + #define GIT_HISTORY_FIXUP_USAGE \\\n    +-\tN_(\"git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]\")\n    ++\tN_(\"git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\")\n    + #define GIT_HISTORY_REWORD_USAGE \\\n    +-\tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\")\n    ++\tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]\")\n    + #define GIT_HISTORY_SPLIT_USAGE \\\n    +-\tN_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\")\n    ++\tN_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]\")\n    +\n    + static void change_data_free(void *util, const char *str UNUSED)\n    + {\n    +@@ builtin/history.c: enum commit_tree_flags {\n    + \tCOMMIT_TREE_EDIT_MESSAGE = (1 << 0),\n    + };\n    +\n    ++static int history_config(const char *var, const char *value,\n    ++\t\t\t  const struct config_context *ctx, void *data)\n    ++{\n    ++\tconst char **sign_commit = data;\n    ++\n    ++\tif (!strcmp(var, \"commit.gpgsign\")) {\n    ++\t\t*sign_commit = git_config_bool(var, value) ? \"\" : NULL;\n    ++\t\treturn 0;\n    ++\t}\n    ++\n    ++\treturn git_default_config(var, value, ctx, NULL);\n    ++}\n    ++\n    ++#define OPT_HISTORY_GPG_SIGN(v) {                 \\\n    ++\t.type = OPTION_STRING,                    \\\n    ++\t.short_name = 'S',                        \\\n    ++\t.long_name = \"gpg-sign\",                  \\\n    ++\t.value = (v),                             \\\n    ++\t.argh = N_(\"key-id\"),                     \\\n    ++\t.help = N_(\"GPG-sign rewritten commits\"), \\\n    ++\t.flags = PARSE_OPT_OPTARG,                \\\n    ++\t.defval = (intptr_t)\"\",                   \\\n    ++}\n    ++\n    + static int commit_tree_ext(struct repository *repo,\n    + \t\t\t   const char *action,\n    + \t\t\t   struct commit *commit_with_message,\n    + \t\t\t   const struct commit_list *parents,\n    + \t\t\t   const struct object_id *old_tree,\n    + \t\t\t   const struct object_id *new_tree,\n    ++\t\t\t   const char *sign_commit,\n    + \t\t\t   struct commit **out,\n    + \t\t\t   enum commit_tree_flags flags)\n    + {\n    +@@ builtin/history.c: static int commit_tree_ext(struct repository *repo,\n    +\n    + \tret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree,\n    + \t\t\t\t   parents, &rewritten_commit_oid, original_author,\n    +-\t\t\t\t   NULL, NULL, original_extra_headers);\n    ++\t\t\t\t   NULL, sign_commit, original_extra_headers);\n    + \tif (ret < 0)\n    + \t\tgoto out;\n    +\n    +@@ builtin/history.c: static int commit_tree_ext(struct repository *repo,\n    + static int commit_tree_with_edited_message(struct repository *repo,\n    + \t\t\t\t\t   const char *action,\n    + \t\t\t\t\t   struct commit *original,\n    ++\t\t\t\t\t   const char *sign_commit,\n    + \t\t\t\t\t   struct commit **out)\n    + {\n    + \tstruct object_id parent_tree_oid;\n    +@@ builtin/history.c: static int commit_tree_with_edited_message(struct repository *repo,\n    + \t}\n    +\n    + \treturn commit_tree_ext(repo, action, original, original->parents,\n    +-\t\t\t       &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);\n    ++\t\t\t       &parent_tree_oid, tree_oid, sign_commit, out,\n    ++\t\t\t       COMMIT_TREE_EDIT_MESSAGE);\n    + }\n    +\n    + enum ref_action {\n    +@@ builtin/history.c: static int compute_pending_ref_updates(struct rev_info *revs,\n    + \t\t\t\t       enum ref_action action,\n    + \t\t\t\t       struct commit *original,\n    + \t\t\t\t       struct commit *rewritten,\n    ++\t\t\t\t       const char *sign_commit,\n    + \t\t\t\t       enum replay_empty_commit_action empty,\n    + \t\t\t\t       struct replay_result *result)\n    + {\n    + \tconst struct name_decoration *decoration;\n    + \tstruct replay_revisions_options opts = {\n    + \t\t.empty = empty,\n    ++\t\t.sign_commit = sign_commit,\n    + \t};\n    + \tchar hex[GIT_MAX_HEXSZ + 1];\n    + \tbool detached_head;\n    +@@ builtin/history.c: static int handle_reference_updates(struct rev_info *revs,\n    + \t\t\t\t    struct commit *rewritten,\n    + \t\t\t\t    const char *reflog_msg,\n    + \t\t\t\t    int dry_run,\n    ++\t\t\t\t    const char *sign_commit,\n    + \t\t\t\t    enum replay_empty_commit_action empty)\n    + {\n    + \tstruct replay_result result = { 0 };\n    + \tint ret;\n    +\n    + \tret = compute_pending_ref_updates(revs, action, original, rewritten,\n    +-\t\t\t\t\t  empty, &result);\n    ++\t\t\t\t\t  sign_commit, empty, &result);\n    + \tif (ret)\n    + \t\tgoto out;\n    +\n    +@@ builtin/history.c: static int cmd_history_fixup(int argc,\n    + \tenum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;\n    + \tenum ref_action action = REF_ACTION_DEFAULT;\n    + \tenum commit_tree_flags flags = 0;\n    ++\tconst char *sign_commit = NULL;\n    + \tint dry_run = 0;\n    + \tstruct option options[] = {\n    + \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n    +@@ builtin/history.c: static int cmd_history_fixup(int argc,\n    + \t\tOPT_CALLBACK_F(0, \"empty\", &empty, \"(drop|keep|abort)\",\n    + \t\t\t       N_(\"how to handle commits that become empty\"),\n    + \t\t\t       PARSE_OPT_NONEG, parse_opt_empty),\n    ++\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n    + \t\tOPT_END(),\n    + \t};\n    + \tstruct merge_result merge_result = { 0 };\n    +@@ builtin/history.c: static int cmd_history_fixup(int argc,\n    + \tbool skip_commit = false;\n    + \tint ret;\n    +\n    ++\trepo_config(repo, history_config, &sign_commit);\n    + \targc = parse_options(argc, argv, prefix, options, usage, 0);\n    + \tif (argc != 1) {\n    + \t\tret = error(_(\"command expects a single revision\"));\n    + \t\tgoto out;\n    + \t}\n    +-\trepo_config(repo, git_default_config, NULL);\n    +\n    + \tif (action == REF_ACTION_DEFAULT)\n    + \t\taction = REF_ACTION_BRANCHES;\n    +@@ builtin/history.c: static int cmd_history_fixup(int argc,\n    + \tif (!skip_commit) {\n    + \t\tret = commit_tree_ext(repo, \"fixup\", original, original->parents,\n    + \t\t\t\t      &original_tree->object.oid, &merge_result.tree->object.oid,\n    +-\t\t\t\t      &rewritten, flags);\n    ++\t\t\t\t      sign_commit, &rewritten, flags);\n    + \t\tif (ret < 0) {\n    + \t\t\tret = error(_(\"failed writing fixed-up commit\"));\n    + \t\t\tgoto out;\n    +@@ builtin/history.c: static int cmd_history_fixup(int argc,\n    + \tstrbuf_addf(&reflog_msg, \"fixup: updating %s\", argv[0]);\n    +\n    + \tret = handle_reference_updates(&revs, action, original, rewritten,\n    +-\t\t\t\t       reflog_msg.buf, dry_run, empty);\n    ++\t\t\t\t       reflog_msg.buf, dry_run, sign_commit, empty);\n    + \tif (ret < 0) {\n    + \t\tret = error(_(\"failed replaying descendants\"));\n    + \t\tgoto out;\n    +@@ builtin/history.c: static int cmd_history_reword(int argc,\n    + \t\tNULL,\n    + \t};\n    + \tenum ref_action action = REF_ACTION_DEFAULT;\n    ++\tconst char *sign_commit = NULL;\n    + \tint dry_run = 0;\n    + \tstruct option options[] = {\n    + \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n    +@@ builtin/history.c: static int cmd_history_reword(int argc,\n    + \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n    + \t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n    + \t\t\t N_(\"perform a dry-run without updating any refs\")),\n    ++\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n    + \t\tOPT_END(),\n    + \t};\n    + \tstruct strbuf reflog_msg = STRBUF_INIT;\n    +@@ builtin/history.c: static int cmd_history_reword(int argc,\n    + \tstruct rev_info revs = { 0 };\n    + \tint ret;\n    +\n    ++\trepo_config(repo, history_config, &sign_commit);\n    + \targc = parse_options(argc, argv, prefix, options, usage, 0);\n    + \tif (argc != 1) {\n    + \t\tret = error(_(\"command expects a single revision\"));\n    + \t\tgoto out;\n    + \t}\n    +-\trepo_config(repo, git_default_config, NULL);\n    +\n    + \tif (action == REF_ACTION_DEFAULT)\n    + \t\taction = REF_ACTION_BRANCHES;\n    +@@ builtin/history.c: static int cmd_history_reword(int argc,\n    + \tif (ret)\n    + \t\tgoto out;\n    +\n    +-\tret = commit_tree_with_edited_message(repo, \"reworded\", original, &rewritten);\n    ++\tret = commit_tree_with_edited_message(repo, \"reworded\", original,\n    ++\t\t\t\t\t      sign_commit, &rewritten);\n    + \tif (ret < 0) {\n    + \t\tret = error(_(\"failed writing reworded commit\"));\n    + \t\tgoto out;\n    +@@ builtin/history.c: static int cmd_history_reword(int argc,\n    + \tstrbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n    +\n    + \tret = handle_reference_updates(&revs, action, original, rewritten,\n    +-\t\t\t\t       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);\n    ++\t\t\t\t       reflog_msg.buf, dry_run, sign_commit,\n    ++\t\t\t\t       REPLAY_EMPTY_COMMIT_ABORT);\n    + \tif (ret < 0) {\n    + \t\tret = error(_(\"failed replaying descendants\"));\n    + \t\tgoto out;\n    +@@ builtin/history.c: static int write_ondisk_index(struct repository *repo,\n    + static int split_commit(struct repository *repo,\n    + \t\t\tstruct commit *original,\n    + \t\t\tstruct pathspec *pathspec,\n    ++\t\t\tconst char *sign_commit,\n    + \t\t\tstruct commit **out)\n    + {\n    + \tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n    +@@ builtin/history.c: static int split_commit(struct repository *repo,\n    + \t * that shall be diffed against is the parent of the original commit.\n    + \t */\n    + \tret = commit_tree_ext(repo, \"split-out\", original, original->parents, &parent_tree_oid,\n    +-\t\t\t      &split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);\n    ++\t\t\t      &split_tree->object.oid, sign_commit, &first_commit,\n    ++\t\t\t      COMMIT_TREE_EDIT_MESSAGE);\n    + \tif (ret < 0) {\n    + \t\tret = error(_(\"failed writing first commit\"));\n    + \t\tgoto out;\n    +@@ builtin/history.c: static int split_commit(struct repository *repo,\n    + \tnew_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;\n    +\n    + \tret = commit_tree_ext(repo, \"split-out\", original, parents, old_tree_oid,\n    +-\t\t\t      new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);\n    ++\t\t\t      new_tree_oid, sign_commit, &second_commit,\n    ++\t\t\t      COMMIT_TREE_EDIT_MESSAGE);\n    + \tif (ret < 0) {\n    + \t\tret = error(_(\"failed writing second commit\"));\n    + \t\tgoto out;\n    +@@ builtin/history.c: static int cmd_history_split(int argc,\n    + \t\tNULL,\n    + \t};\n    + \tenum ref_action action = REF_ACTION_DEFAULT;\n    ++\tconst char *sign_commit = NULL;\n    + \tint dry_run = 0;\n    + \tstruct option options[] = {\n    + \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n    +@@ builtin/history.c: static int cmd_history_split(int argc,\n    + \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n    + \t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n    + \t\t\t N_(\"perform a dry-run without updating any refs\")),\n    ++\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n    + \t\tOPT_END(),\n    + \t};\n    + \tstruct commit *original, *rewritten = NULL;\n    +@@ builtin/history.c: static int cmd_history_split(int argc,\n    + \tstruct rev_info revs = { 0 };\n    + \tint ret;\n    +\n    ++\trepo_config(repo, history_config, &sign_commit);\n    + \targc = parse_options(argc, argv, prefix, options, usage, 0);\n    + \tif (argc < 1) {\n    + \t\tret = error(_(\"command expects a committish\"));\n    + \t\tgoto out;\n    + \t}\n    +-\trepo_config(repo, git_default_config, NULL);\n    +\n    + \tif (action == REF_ACTION_DEFAULT)\n    + \t\taction = REF_ACTION_BRANCHES;\n    +@@ builtin/history.c: static int cmd_history_split(int argc,\n    + \t\tgoto out;\n    + \t}\n    +\n    +-\tret = split_commit(repo, original, &pathspec, &rewritten);\n    ++\tret = split_commit(repo, original, &pathspec, sign_commit, &rewritten);\n    + \tif (ret < 0)\n    + \t\tgoto out;\n    +\n    + \tstrbuf_addf(&reflog_msg, \"split: updating %s\", argv[0]);\n    +\n    + \tret = handle_reference_updates(&revs, action, original, rewritten,\n    +-\t\t\t\t       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);\n    ++\t\t\t\t       reflog_msg.buf, dry_run, sign_commit,\n    ++\t\t\t\t       REPLAY_EMPTY_COMMIT_ABORT);\n    + \tif (ret < 0) {\n    + \t\tret = error(_(\"failed replaying descendants\"));\n    + \t\tgoto out;\n    +@@ builtin/history.c: static int cmd_history_drop(int argc,\n    + \t};\n    + \tenum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;\n    + \tenum ref_action action = REF_ACTION_DEFAULT;\n    ++\tconst char *sign_commit = NULL;\n    + \tint dry_run = 0;\n    + \tstruct option options[] = {\n    + \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n    +@@ builtin/history.c: static int cmd_history_drop(int argc,\n    + \t\tOPT_CALLBACK_F(0, \"empty\", &empty, \"(drop|keep|abort)\",\n    + \t\t\t       N_(\"how to handle descendants that become empty\"),\n    + \t\t\t       PARSE_OPT_NONEG, parse_opt_empty),\n    ++\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n    + \t\tOPT_END(),\n    + \t};\n    + \tstruct strbuf reflog_msg = STRBUF_INIT;\n    +@@ builtin/history.c: static int cmd_history_drop(int argc,\n    + \tbool head_moves = false;\n    + \tint ret;\n    +\n    ++\trepo_config(repo, history_config, &sign_commit);\n    + \targc = parse_options(argc, argv, prefix, options, usage, 0);\n    + \tif (argc != 1) {\n    + \t\tret = error(_(\"command expects a single revision\"));\n    + \t\tgoto out;\n    + \t}\n    +-\trepo_config(repo, git_default_config, NULL);\n    +\n    + \tif (action == REF_ACTION_DEFAULT)\n    + \t\taction = REF_ACTION_BRANCHES;\n    +@@ builtin/history.c: static int cmd_history_drop(int argc,\n    + \trewritten = original->parents->item;\n    +\n    + \tret = compute_pending_ref_updates(&revs, action, original, rewritten,\n    +-\t\t\t\t\t  empty, &result);\n    ++\t\t\t\t\t  sign_commit, empty, &result);\n    + \tif (ret) {\n    + \t\tret = error(_(\"failed replaying descendants\"));\n    + \t\tgoto out;\n    +\n      ## t/t3451-history-reword.sh ##\n     @@ t/t3451-history-reword.sh: test_description='tests for git-history reword subcommand'\n\n    @@ t/t3451-history-reword.sh: test_expect_success 'can reword commit in the middle'\n     +test_reword_gpg_sign ! true  --no-gpg-sign\n     +test_reword_gpg_sign ! true  --gpg-sign --no-gpg-sign\n     +test_reword_gpg_sign   false --no-gpg-sign --gpg-sign\n    ++\n    ++test_expect_success GPG 'reword uses an explicit signing key for rewritten history' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tgit init repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\ttest_commit first &&\n    ++\t\ttest_commit second &&\n    ++\t\ttest_commit third &&\n    ++\n    ++\t\treword_with_message -SB7227189 HEAD~ <<-EOF &&\n    ++\t\tsecond reworded\n    ++\t\tEOF\n    ++\n    ++\t\tgit verify-commit HEAD~ &&\n    ++\t\tgit verify-commit HEAD &&\n    ++\t\tgit log -2 --format=%GK >actual &&\n    ++\t\tcat >expect <<-\\EOF &&\n    ++\t\t65A0EEA02E30CAD7\n    ++\t\t65A0EEA02E30CAD7\n    ++\t\tEOF\n    ++\t\ttest_cmp expect actual\n    ++\t)\n    ++'\n     +\n      test_expect_success 'can reword commit in the middle even on detached head' '\n      \ttest_when_finished \"rm -rf repo\" &&\n    @@ t/t3453-history-fixup.sh: test_expect_success 'preserves commit message and auth\n      test_expect_success 'updates all descendant branches by default' '\n      \ttest_when_finished \"rm -rf repo\" &&\n      \tgit init repo --initial-branch=main &&\n    +\n    + ## t/t3454-history-drop.sh ##\n    +@@ t/t3454-history-drop.sh: test_description='tests for git-history drop subcommand'\n    +\n    + . ./test-lib.sh\n    + . \"$TEST_DIRECTORY/lib-log-graph.sh\"\n    ++. \"$TEST_DIRECTORY/lib-gpg.sh\"\n    +\n    + expect_graph () {\n    + \tcat >expect &&\n    +@@ t/t3454-history-drop.sh: expect_log () {\n    + \ttest_cmp expect actual\n    + }\n    +\n    ++test_drop_gpg_sign () {\n    ++\tmust_fail= will=will\n    ++\tif test \"x$1\" = \"x!\"\n    ++\tthen\n    ++\t\tmust_fail=test_must_fail\n    ++\t\twill=\"will not\"\n    ++\t\tshift\n    ++\tfi\n    ++\tconf=$1\n    ++\tshift\n    ++\n    ++\ttest_expect_success GPG \"drop $* with commit.gpgsign=$conf $will sign replayed descendants\" \"\n    ++\t\ttest_when_finished 'rm -rf repo' &&\n    ++\t\tgit init repo &&\n    ++\t\t(\n    ++\t\t\tcd repo &&\n    ++\t\t\ttest_commit first &&\n    ++\t\t\ttest_commit second &&\n    ++\t\t\ttest_commit third &&\n    ++\n    ++\t\t\tgit config commit.gpgsign $conf &&\n    ++\t\t\tgit history drop $* HEAD~ &&\n    ++\n    ++\t\t\t$must_fail git verify-commit HEAD\n    ++\t\t)\n    ++\t\"\n    ++}\n    ++\n    + test_expect_success 'errors on missing commit argument' '\n    + \ttest_when_finished \"rm -rf repo\" &&\n    + \tgit init repo &&\n    +@@ t/t3454-history-drop.sh: test_expect_success 'drops a commit in the middle and replays descendants' '\n    + \t)\n    + '\n    +\n    ++test_drop_gpg_sign ! false\n    ++test_drop_gpg_sign   true\n    ++test_drop_gpg_sign   false --gpg-sign\n    ++test_drop_gpg_sign ! true  --no-gpg-sign\n    ++test_drop_gpg_sign ! true  --gpg-sign --no-gpg-sign\n    ++test_drop_gpg_sign   false --no-gpg-sign --gpg-sign\n    ++\n    ++test_expect_success GPG 'drop has no commit to sign when dropping the tip' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tgit init repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\ttest_commit first &&\n    ++\t\ttest_commit second &&\n    ++\n    ++\t\tgit history drop --gpg-sign HEAD &&\n    ++\n    ++\t\ttest_must_fail git verify-commit HEAD\n    ++\t)\n    ++'\n    ++\n    + test_expect_success 'drops the HEAD commit' '\n    + \ttest_when_finished \"rm -rf repo\" &&\n    + \tgit init repo &&\n--\n2.55.0\n\n"},{"id":"548514","messageId":"20260717145142.39478-2-git@5ouma.me","threadId":"65920","inReplyTo":"20260703145037.69832-1-git@5ouma.me","subject":"[PATCH v2 1/2] replay: allow callers to sign commits","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-07-17T14:51:41Z","receivedAt":"2026-07-17T14:52:07Z","isPatch":true,"body":"The replay machinery creates commits directly through\n`commit_tree_extended()`, but callers cannot currently request\nsignatures. Commands that replay rewritten history consequently cannot\ncarry their signing policy through to descendant commits.\n\nAdd `sign_commit` to `replay_revisions_options` and thread it through\ncommit creation. `NULL` preserves the existing unsigned behavior, an\nempty string selects the default signing key, and a non-empty string\nselects an explicit key. Existing callers zero-initialize the options\nstructure, so their behavior is unchanged.\n\nSigned-off-by: Souma <git@5ouma.me>\n---\n replay.c | 13 ++++++++-----\n replay.h |  6 ++++++\n 2 files changed, 14 insertions(+), 5 deletions(-)\n\ndiff --git a/replay.c b/replay.c\nindex aac9178875..19a6402bf0 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -81,13 +81,13 @@ static struct commit *create_commit(struct repository *repo,\n \t\t\t\t    struct tree *tree,\n \t\t\t\t    struct commit *based_on,\n \t\t\t\t    struct commit *parent,\n-\t\t\t\t    enum replay_mode mode)\n+\t\t\t\t    enum replay_mode mode,\n+\t\t\t\t    const char *sign_commit)\n {\n \tstruct object_id ret;\n \tstruct object *obj = NULL;\n \tstruct commit_list *parents = NULL;\n \tchar *author = NULL;\n-\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n \tstruct commit_extra_header *extra = NULL;\n \tstruct strbuf msg = STRBUF_INIT;\n \tconst char *out_enc = get_commit_output_encoding();\n@@ -270,7 +270,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \t\t\t\t\t  struct merge_options *merge_opt,\n \t\t\t\t\t  struct merge_result *result,\n \t\t\t\t\t  enum replay_mode mode,\n-\t\t\t\t\t  enum replay_empty_commit_action empty)\n+\t\t\t\t\t  enum replay_empty_commit_action empty,\n+\t\t\t\t\t  const char *sign_commit)\n {\n \tstruct commit *base, *replayed_base;\n \tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n@@ -341,7 +342,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \t\t}\n \t}\n \n-\treturn create_commit(repo, result->tree, pickme, replayed_base, mode);\n+\treturn create_commit(repo, result->tree, pickme, replayed_base, mode,\n+\t\t\t     sign_commit);\n }\n \n void replay_result_release(struct replay_result *result)\n@@ -431,7 +433,8 @@ int replay_revisions(struct rev_info *revs,\n \n \t\tlast_commit = pick_regular_commit(revs->repo, commit, replayed_commits,\n \t\t\t\t\t\t  mode == REPLAY_MODE_REVERT ? last_commit : onto,\n-\t\t\t\t\t\t  &merge_opt, &result, mode, opts->empty);\n+\t\t\t\t\t\t  &merge_opt, &result, mode, opts->empty,\n+\t\t\t\t\t\t  opts->sign_commit);\n \t\tif (!last_commit)\n \t\t\tbreak;\n \ndiff --git a/replay.h b/replay.h\nindex 491db145e3..6ed0608911 100644\n--- a/replay.h\n+++ b/replay.h\n@@ -57,6 +57,12 @@ struct replay_revisions_options {\n \t */\n \tint contained;\n \n+\t/*\n+\t * Key used to sign newly-created commits. An empty string requests the\n+\t * default configured signing key, and NULL disables signing.\n+\t */\n+\tconst char *sign_commit;\n+\n \t/*\n \t * Controls what to do when a replayed commit becomes empty.\n \t * Defaults to REPLAY_EMPTY_COMMIT_DROP.\n-- \n2.55.0\n\n"},{"id":"548515","messageId":"20260717145142.39478-3-git@5ouma.me","threadId":"65920","inReplyTo":"20260703145037.69832-1-git@5ouma.me","subject":"[PATCH v2 2/2] builtin/history: sign rewritten commits","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-07-17T14:51:42Z","receivedAt":"2026-07-17T14:52:11Z","isPatch":true,"body":"The history commands create replacement commits directly instead of\nusing the sequencer or the commit porcelain. As a result, rewritten\ncommits ignore `commit.gpgSign` and cannot be signed on demand.\n\nRead the signing configuration before parsing options so that it\nestablishes the default and later `-S`/`--gpg-sign` or `--no-gpg-sign`\noptions override it. Pass the selected key through direct rewrites and\nthe replay machinery.\n\nSign every newly created commit, including both halves of a split and\nreplayed descendants. Dropping the tip creates no replacement commit,\nso there is nothing to sign. As with `rebase --gpg-sign`, the signature\nrecords the attestation of the current committer to the rewritten\ncommit while retaining the original author identity; it does not claim\nauthorship of commits written by somebody else.\n\nDocument the behavior and add GPG-gated coverage for configuration,\ncommand-line overrides, last-option-wins precedence, replayed\ndescendants, split commits, an explicit signing key, and the\nno-new-commit drop case.\n\nSigned-off-by: Souma <git@5ouma.me>\n---\n Documentation/git-history.adoc | 16 +++++--\n builtin/history.c              | 84 ++++++++++++++++++++++++++--------\n t/t3451-history-reword.sh      | 63 +++++++++++++++++++++++++\n t/t3452-history-split.sh       | 44 ++++++++++++++++++\n t/t3453-history-fixup.sh       | 39 ++++++++++++++++\n t/t3454-history-drop.sh        | 50 ++++++++++++++++++++\n 6 files changed, 272 insertions(+), 24 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 28b477cd37..8345cced4c 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,10 +8,10 @@ git-history - EXPERIMENTAL: Rewrite history\n SYNOPSIS\n --------\n [synopsis]\n-git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]\n-git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]\n-git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\n-git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\n+git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\n+git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\n+git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]\n+git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]\n \n DESCRIPTION\n -----------\n@@ -125,6 +125,14 @@ OPTIONS\n `--reedit-message`::\n \tOpen an editor to modify the target commit's message.\n \n+`-S[<key-id>]`::\n+`--gpg-sign[=<key-id>]`::\n+`--no-gpg-sign`::\n+\tGPG-sign rewritten commits. The _<key-id>_ argument is optional and\n+\tdefaults to the committer identity; if specified, it must be stuck to\n+\tthe option without a space. `--no-gpg-sign` is useful to countermand\n+\tboth `commit.gpgSign` configuration and earlier `--gpg-sign`.\n+\n `--empty=(drop|keep|abort)`::\n \tControl what happens when a commit becomes empty as a result of the\n \tfixup. This can happen in two situations:\ndiff --git a/builtin/history.c b/builtin/history.c\nindex d28c1f08bb..97e0d77013 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -27,13 +27,13 @@\n #include \"wt-status.h\"\n \n #define GIT_HISTORY_DROP_USAGE \\\n-\tN_(\"git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]\")\n+\tN_(\"git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\")\n #define GIT_HISTORY_FIXUP_USAGE \\\n-\tN_(\"git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]\")\n+\tN_(\"git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\")\n #define GIT_HISTORY_REWORD_USAGE \\\n-\tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\")\n+\tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]\")\n #define GIT_HISTORY_SPLIT_USAGE \\\n-\tN_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\")\n+\tN_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]\")\n \n static void change_data_free(void *util, const char *str UNUSED)\n {\n@@ -105,12 +105,37 @@ enum commit_tree_flags {\n \tCOMMIT_TREE_EDIT_MESSAGE = (1 << 0),\n };\n \n+static int history_config(const char *var, const char *value,\n+\t\t\t  const struct config_context *ctx, void *data)\n+{\n+\tconst char **sign_commit = data;\n+\n+\tif (!strcmp(var, \"commit.gpgsign\")) {\n+\t\t*sign_commit = git_config_bool(var, value) ? \"\" : NULL;\n+\t\treturn 0;\n+\t}\n+\n+\treturn git_default_config(var, value, ctx, NULL);\n+}\n+\n+#define OPT_HISTORY_GPG_SIGN(v) {                 \\\n+\t.type = OPTION_STRING,                    \\\n+\t.short_name = 'S',                        \\\n+\t.long_name = \"gpg-sign\",                  \\\n+\t.value = (v),                             \\\n+\t.argh = N_(\"key-id\"),                     \\\n+\t.help = N_(\"GPG-sign rewritten commits\"), \\\n+\t.flags = PARSE_OPT_OPTARG,                \\\n+\t.defval = (intptr_t)\"\",                   \\\n+}\n+\n static int commit_tree_ext(struct repository *repo,\n \t\t\t   const char *action,\n \t\t\t   struct commit *commit_with_message,\n \t\t\t   const struct commit_list *parents,\n \t\t\t   const struct object_id *old_tree,\n \t\t\t   const struct object_id *new_tree,\n+\t\t\t   const char *sign_commit,\n \t\t\t   struct commit **out,\n \t\t\t   enum commit_tree_flags flags)\n {\n@@ -151,7 +176,7 @@ static int commit_tree_ext(struct repository *repo,\n \n \tret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree,\n \t\t\t\t   parents, &rewritten_commit_oid, original_author,\n-\t\t\t\t   NULL, NULL, original_extra_headers);\n+\t\t\t\t   NULL, sign_commit, original_extra_headers);\n \tif (ret < 0)\n \t\tgoto out;\n \n@@ -167,6 +192,7 @@ static int commit_tree_ext(struct repository *repo,\n static int commit_tree_with_edited_message(struct repository *repo,\n \t\t\t\t\t   const char *action,\n \t\t\t\t\t   struct commit *original,\n+\t\t\t\t\t   const char *sign_commit,\n \t\t\t\t\t   struct commit **out)\n {\n \tstruct object_id parent_tree_oid;\n@@ -188,7 +214,8 @@ static int commit_tree_with_edited_message(struct repository *repo,\n \t}\n \n \treturn commit_tree_ext(repo, action, original, original->parents,\n-\t\t\t       &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);\n+\t\t\t       &parent_tree_oid, tree_oid, sign_commit, out,\n+\t\t\t       COMMIT_TREE_EDIT_MESSAGE);\n }\n \n enum ref_action {\n@@ -344,12 +371,14 @@ static int compute_pending_ref_updates(struct rev_info *revs,\n \t\t\t\t       enum ref_action action,\n \t\t\t\t       struct commit *original,\n \t\t\t\t       struct commit *rewritten,\n+\t\t\t\t       const char *sign_commit,\n \t\t\t\t       enum replay_empty_commit_action empty,\n \t\t\t\t       struct replay_result *result)\n {\n \tconst struct name_decoration *decoration;\n \tstruct replay_revisions_options opts = {\n \t\t.empty = empty,\n+\t\t.sign_commit = sign_commit,\n \t};\n \tchar hex[GIT_MAX_HEXSZ + 1];\n \tbool detached_head;\n@@ -454,13 +483,14 @@ static int handle_reference_updates(struct rev_info *revs,\n \t\t\t\t    struct commit *rewritten,\n \t\t\t\t    const char *reflog_msg,\n \t\t\t\t    int dry_run,\n+\t\t\t\t    const char *sign_commit,\n \t\t\t\t    enum replay_empty_commit_action empty)\n {\n \tstruct replay_result result = { 0 };\n \tint ret;\n \n \tret = compute_pending_ref_updates(revs, action, original, rewritten,\n-\t\t\t\t\t  empty, &result);\n+\t\t\t\t\t  sign_commit, empty, &result);\n \tif (ret)\n \t\tgoto out;\n \n@@ -522,6 +552,7 @@ static int cmd_history_fixup(int argc,\n \tenum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;\n \tenum ref_action action = REF_ACTION_DEFAULT;\n \tenum commit_tree_flags flags = 0;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -535,6 +566,7 @@ static int cmd_history_fixup(int argc,\n \t\tOPT_CALLBACK_F(0, \"empty\", &empty, \"(drop|keep|abort)\",\n \t\t\t       N_(\"how to handle commits that become empty\"),\n \t\t\t       PARSE_OPT_NONEG, parse_opt_empty),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct merge_result merge_result = { 0 };\n@@ -546,12 +578,12 @@ static int cmd_history_fixup(int argc,\n \tbool skip_commit = false;\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc != 1) {\n \t\tret = error(_(\"command expects a single revision\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -676,7 +708,7 @@ static int cmd_history_fixup(int argc,\n \tif (!skip_commit) {\n \t\tret = commit_tree_ext(repo, \"fixup\", original, original->parents,\n \t\t\t\t      &original_tree->object.oid, &merge_result.tree->object.oid,\n-\t\t\t\t      &rewritten, flags);\n+\t\t\t\t      sign_commit, &rewritten, flags);\n \t\tif (ret < 0) {\n \t\t\tret = error(_(\"failed writing fixed-up commit\"));\n \t\t\tgoto out;\n@@ -686,7 +718,7 @@ static int cmd_history_fixup(int argc,\n \tstrbuf_addf(&reflog_msg, \"fixup: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf, dry_run, empty);\n+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit, empty);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\n@@ -711,6 +743,7 @@ static int cmd_history_reword(int argc,\n \t\tNULL,\n \t};\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -718,6 +751,7 @@ static int cmd_history_reword(int argc,\n \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n \t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n \t\t\t N_(\"perform a dry-run without updating any refs\")),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n@@ -725,12 +759,12 @@ static int cmd_history_reword(int argc,\n \tstruct rev_info revs = { 0 };\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc != 1) {\n \t\tret = error(_(\"command expects a single revision\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -745,7 +779,8 @@ static int cmd_history_reword(int argc,\n \tif (ret)\n \t\tgoto out;\n \n-\tret = commit_tree_with_edited_message(repo, \"reworded\", original, &rewritten);\n+\tret = commit_tree_with_edited_message(repo, \"reworded\", original,\n+\t\t\t\t\t      sign_commit, &rewritten);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing reworded commit\"));\n \t\tgoto out;\n@@ -754,7 +789,8 @@ static int cmd_history_reword(int argc,\n \tstrbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);\n+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit,\n+\t\t\t\t       REPLAY_EMPTY_COMMIT_ABORT);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\n@@ -816,6 +852,7 @@ static int write_ondisk_index(struct repository *repo,\n static int split_commit(struct repository *repo,\n \t\t\tstruct commit *original,\n \t\t\tstruct pathspec *pathspec,\n+\t\t\tconst char *sign_commit,\n \t\t\tstruct commit **out)\n {\n \tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n@@ -893,7 +930,8 @@ static int split_commit(struct repository *repo,\n \t * that shall be diffed against is the parent of the original commit.\n \t */\n \tret = commit_tree_ext(repo, \"split-out\", original, original->parents, &parent_tree_oid,\n-\t\t\t      &split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);\n+\t\t\t      &split_tree->object.oid, sign_commit, &first_commit,\n+\t\t\t      COMMIT_TREE_EDIT_MESSAGE);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing first commit\"));\n \t\tgoto out;\n@@ -910,7 +948,8 @@ static int split_commit(struct repository *repo,\n \tnew_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;\n \n \tret = commit_tree_ext(repo, \"split-out\", original, parents, old_tree_oid,\n-\t\t\t      new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);\n+\t\t\t      new_tree_oid, sign_commit, &second_commit,\n+\t\t\t      COMMIT_TREE_EDIT_MESSAGE);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing second commit\"));\n \t\tgoto out;\n@@ -938,6 +977,7 @@ static int cmd_history_split(int argc,\n \t\tNULL,\n \t};\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -945,6 +985,7 @@ static int cmd_history_split(int argc,\n \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n \t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n \t\t\t N_(\"perform a dry-run without updating any refs\")),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct commit *original, *rewritten = NULL;\n@@ -953,12 +994,12 @@ static int cmd_history_split(int argc,\n \tstruct rev_info revs = { 0 };\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc < 1) {\n \t\tret = error(_(\"command expects a committish\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -984,14 +1025,15 @@ static int cmd_history_split(int argc,\n \t\tgoto out;\n \t}\n \n-\tret = split_commit(repo, original, &pathspec, &rewritten);\n+\tret = split_commit(repo, original, &pathspec, sign_commit, &rewritten);\n \tif (ret < 0)\n \t\tgoto out;\n \n \tstrbuf_addf(&reflog_msg, \"split: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);\n+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit,\n+\t\t\t\t       REPLAY_EMPTY_COMMIT_ABORT);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\n@@ -1081,6 +1123,7 @@ static int cmd_history_drop(int argc,\n \t};\n \tenum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -1091,6 +1134,7 @@ static int cmd_history_drop(int argc,\n \t\tOPT_CALLBACK_F(0, \"empty\", &empty, \"(drop|keep|abort)\",\n \t\t\t       N_(\"how to handle descendants that become empty\"),\n \t\t\t       PARSE_OPT_NONEG, parse_opt_empty),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n@@ -1101,12 +1145,12 @@ static int cmd_history_drop(int argc,\n \tbool head_moves = false;\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc != 1) {\n \t\tret = error(_(\"command expects a single revision\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -1134,7 +1178,7 @@ static int cmd_history_drop(int argc,\n \trewritten = original->parents->item;\n \n \tret = compute_pending_ref_updates(&revs, action, original, rewritten,\n-\t\t\t\t\t  empty, &result);\n+\t\t\t\t\t  sign_commit, empty, &result);\n \tif (ret) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex de7b357685..6dbe2143d3 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -4,6 +4,7 @@ test_description='tests for git-history reword subcommand'\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY/lib-log-graph.sh\"\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n reword_with_message () {\n \tcat >message &&\n@@ -26,6 +27,37 @@ expect_log () {\n \ttest_cmp expect actual\n }\n \n+test_reword_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"reword $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit first &&\n+\t\t\ttest_commit second &&\n+\t\t\ttest_commit third &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\treword_with_message $* HEAD~ <<-EOF &&\n+\t\t\tsecond reworded\n+\t\t\tEOF\n+\n+\t\t\t$must_fail git verify-commit HEAD~ &&\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'can reword tip of a branch' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -77,6 +109,37 @@ test_expect_success 'can reword commit in the middle' '\n \t)\n '\n \n+test_reword_gpg_sign ! false\n+test_reword_gpg_sign   true\n+test_reword_gpg_sign   false --gpg-sign\n+test_reword_gpg_sign ! true  --no-gpg-sign\n+test_reword_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_reword_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n+test_expect_success GPG 'reword uses an explicit signing key for rewritten history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\treword_with_message -SB7227189 HEAD~ <<-EOF &&\n+\t\tsecond reworded\n+\t\tEOF\n+\n+\t\tgit verify-commit HEAD~ &&\n+\t\tgit verify-commit HEAD &&\n+\t\tgit log -2 --format=%GK >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\t65A0EEA02E30CAD7\n+\t\t65A0EEA02E30CAD7\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n test_expect_success 'can reword commit in the middle even on detached head' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\ndiff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh\nindex 8ed0cebb50..e96f492cc6 100755\n--- a/t/t3452-history-split.sh\n+++ b/t/t3452-history-split.sh\n@@ -4,6 +4,7 @@ test_description='tests for git-history split subcommand'\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY/lib-log-graph.sh\"\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n # The fake editor takes multiple arguments, each of which represents a commit\n # message. Subsequent invocations of the editor will then yield those messages\n@@ -36,6 +37,42 @@ expect_tree_entries () {\n \ttest_cmp expect actual\n }\n \n+test_split_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"split $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit initial &&\n+\t\t\ttouch bar foo &&\n+\t\t\tgit add . &&\n+\t\t\tgit commit -m split-me &&\n+\t\t\ttest_commit tip &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\tset_fake_editor 'first' 'second' &&\n+\t\t\tgit history split $* HEAD~ <<-EOF &&\n+\t\t\ty\n+\t\t\tn\n+\t\t\tEOF\n+\n+\t\t\t$must_fail git verify-commit HEAD~2 &&\n+\t\t\t$must_fail git verify-commit HEAD~ &&\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'refuses to work with merge commits' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -141,6 +178,13 @@ test_expect_success 'can split up tip commit' '\n \t)\n '\n \n+test_split_gpg_sign ! false\n+test_split_gpg_sign   true\n+test_split_gpg_sign   false --gpg-sign\n+test_split_gpg_sign ! true  --no-gpg-sign\n+test_split_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_split_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n test_expect_success 'can split up root commit' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\ndiff --git a/t/t3453-history-fixup.sh b/t/t3453-history-fixup.sh\nindex 868298e248..cd20a23115 100755\n--- a/t/t3453-history-fixup.sh\n+++ b/t/t3453-history-fixup.sh\n@@ -3,6 +3,7 @@\n test_description='tests for git-history fixup subcommand'\n \n . ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n fixup_with_message () {\n \tcat >message &&\n@@ -21,6 +22,37 @@ expect_changes () {\n \ttest_cmp expect actual\n }\n \n+test_fixup_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"fixup $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit first &&\n+\t\t\ttest_commit second &&\n+\t\t\ttest_commit third &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\techo fix >>second.t &&\n+\t\t\tgit add second.t &&\n+\t\t\tgit history fixup $* HEAD~ &&\n+\n+\t\t\t$must_fail git verify-commit HEAD~ &&\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'errors on missing commit argument' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -229,6 +261,13 @@ test_expect_success 'preserves commit message and authorship' '\n \t)\n '\n \n+test_fixup_gpg_sign ! false\n+test_fixup_gpg_sign   true\n+test_fixup_gpg_sign   false --gpg-sign\n+test_fixup_gpg_sign ! true  --no-gpg-sign\n+test_fixup_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_fixup_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n test_expect_success 'updates all descendant branches by default' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo --initial-branch=main &&\ndiff --git a/t/t3454-history-drop.sh b/t/t3454-history-drop.sh\nindex 68a86d1e37..5b21078a7e 100755\n--- a/t/t3454-history-drop.sh\n+++ b/t/t3454-history-drop.sh\n@@ -4,6 +4,7 @@ test_description='tests for git-history drop subcommand'\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY/lib-log-graph.sh\"\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n expect_graph () {\n \tcat >expect &&\n@@ -16,6 +17,34 @@ expect_log () {\n \ttest_cmp expect actual\n }\n \n+test_drop_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"drop $* with commit.gpgsign=$conf $will sign replayed descendants\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit first &&\n+\t\t\ttest_commit second &&\n+\t\t\ttest_commit third &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\tgit history drop $* HEAD~ &&\n+\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'errors on missing commit argument' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -88,6 +117,27 @@ test_expect_success 'drops a commit in the middle and replays descendants' '\n \t)\n '\n \n+test_drop_gpg_sign ! false\n+test_drop_gpg_sign   true\n+test_drop_gpg_sign   false --gpg-sign\n+test_drop_gpg_sign ! true  --no-gpg-sign\n+test_drop_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_drop_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n+test_expect_success GPG 'drop has no commit to sign when dropping the tip' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\n+\t\tgit history drop --gpg-sign HEAD &&\n+\n+\t\ttest_must_fail git verify-commit HEAD\n+\t)\n+'\n+\n test_expect_success 'drops the HEAD commit' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n-- \n2.55.0\n\n"},{"id":"552523","messageId":"aqO0fcF-feQHIjks@pks.im","threadId":"65920","inReplyTo":"20260717145142.39478-2-git@5ouma.me","subject":"Re: [PATCH v2 1/2] replay: allow callers to sign commits","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-09-11T07:57:49Z","receivedAt":"2026-09-11T07:57:55Z","isPatch":true,"body":"On Fri, Jul 17, 2026 at 02:51:41PM +0000, Souma wrote:\n\nSorry for taking so long to review this, I lost track of this series.\n\n> The replay machinery creates commits directly through\n> `commit_tree_extended()`, but callers cannot currently request\n> signatures. Commands that replay rewritten history consequently cannot\n> carry their signing policy through to descendant commits.\n> \n> Add `sign_commit` to `replay_revisions_options` and thread it through\n> commit creation. `NULL` preserves the existing unsigned behavior, an\n> empty string selects the default signing key, and a non-empty string\n> selects an explicit key. Existing callers zero-initialize the options\n> structure, so their behavior is unchanged.\n\nNit: I feel like documenting the exact behaviour of that parameter here\nis a bit excessive. You document it in-code, which is sufficient.\n\nThe changes themselves look good to me.\n\nPatrick\n"},{"id":"552524","messageId":"aqO0hsYNgCwm2_UY@pks.im","threadId":"65920","inReplyTo":"20260717145142.39478-3-git@5ouma.me","subject":"Re: [PATCH v2 2/2] builtin/history: sign rewritten commits","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-09-11T07:57:58Z","receivedAt":"2026-09-11T07:58:02Z","isPatch":true,"body":"On Fri, Jul 17, 2026 at 02:51:42PM +0000, Souma wrote:\n> The history commands create replacement commits directly instead of\n> using the sequencer or the commit porcelain. As a result, rewritten\n> commits ignore `commit.gpgSign` and cannot be signed on demand.\n> \n> Read the signing configuration before parsing options so that it\n> establishes the default and later `-S`/`--gpg-sign` or `--no-gpg-sign`\n> options override it. Pass the selected key through direct rewrites and\n> the replay machinery.\n> \n> Sign every newly created commit, including both halves of a split and\n> replayed descendants. Dropping the tip creates no replacement commit,\n> so there is nothing to sign.\n\nNit: this sentence doesn't really add much value, I think, as it just\ncovers a small edge case. It might even briefly derail the reader as\nthey might wonder whether we ever have to sign with the \"drop\"\nsubcommand.\n\n> As with `rebase --gpg-sign`, the signature\n> records the attestation of the current committer to the rewritten\n> commit while retaining the original author identity; it does not claim\n> authorship of commits written by somebody else.\n> \n> Document the behavior and add GPG-gated coverage for configuration,\n> command-line overrides, last-option-wins precedence, replayed\n> descendants, split commits, an explicit signing key, and the\n> no-new-commit drop case.\n\nThis paragraph doesn't add much value and can be dropped entirely.\n\n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> index 28b477cd37..8345cced4c 100644\n> --- a/Documentation/git-history.adoc\n> +++ b/Documentation/git-history.adoc\n> @@ -125,6 +125,14 @@ OPTIONS\n>  `--reedit-message`::\n>  \tOpen an editor to modify the target commit's message.\n>  \n> +`-S[<key-id>]`::\n> +`--gpg-sign[=<key-id>]`::\n> +`--no-gpg-sign`::\n> +\tGPG-sign rewritten commits. The _<key-id>_ argument is optional and\n> +\tdefaults to the committer identity; if specified, it must be stuck to\n> +\tthe option without a space. `--no-gpg-sign` is useful to countermand\n> +\tboth `commit.gpgSign` configuration and earlier `--gpg-sign`.\n> +\n>  `--empty=(drop|keep|abort)`::\n>  \tControl what happens when a commit becomes empty as a result of the\n>  \tfixup. This can happen in two situations:\n\nThis matches what we have in git-rebase(1). Good.\n\n> diff --git a/builtin/history.c b/builtin/history.c\n> index d28c1f08bb..97e0d77013 100644\n> --- a/builtin/history.c\n> +++ b/builtin/history.c\n> @@ -105,12 +105,37 @@ enum commit_tree_flags {\n>  \tCOMMIT_TREE_EDIT_MESSAGE = (1 << 0),\n>  };\n>  \n> +static int history_config(const char *var, const char *value,\n> +\t\t\t  const struct config_context *ctx, void *data)\n> +{\n> +\tconst char **sign_commit = data;\n> +\n> +\tif (!strcmp(var, \"commit.gpgsign\")) {\n> +\t\t*sign_commit = git_config_bool(var, value) ? \"\" : NULL;\n> +\t\treturn 0;\n> +\t}\n> +\n> +\treturn git_default_config(var, value, ctx, NULL);\n> +}\n> +\n> +#define OPT_HISTORY_GPG_SIGN(v) {                 \\\n> +\t.type = OPTION_STRING,                    \\\n> +\t.short_name = 'S',                        \\\n> +\t.long_name = \"gpg-sign\",                  \\\n> +\t.value = (v),                             \\\n> +\t.argh = N_(\"key-id\"),                     \\\n> +\t.help = N_(\"GPG-sign rewritten commits\"), \\\n> +\t.flags = PARSE_OPT_OPTARG,                \\\n> +\t.defval = (intptr_t)\"\",                   \\\n> +}\n\nStyle: we don't align the `\\` character.\n\nPatrick\n"},{"id":"552623","messageId":"20260912160045.36064-1-git@5ouma.me","threadId":"65920","inReplyTo":"20260703145037.69832-1-git@5ouma.me","subject":"[PATCH v3 0/2] history: support signing rewritten commits","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-09-12T16:00:43Z","receivedAt":"2026-09-12T16:00:58Z","isPatch":true,"body":"The history commands create commits directly and via the replay\nmachinery, but currently have no way to honor `commit.gpgSign` or an\nexplicit signing request. This means users who require signed commits\nlose that property when rewriting history.\n\nTeach the replay API to accept a signing key, then expose the standard\n`-S`/`--gpg-sign[=<key-id>]` and `--no-gpg-sign` interface across the\n`git history drop`, `git history fixup`, `git history reword`, and `git\nhistory split` subcommands. The selected policy applies to every new\ncommit, including both halves of a split and replayed descendants.\n\nThe implementation follows the precedence used by rebase, cherry-pick,\nand revert: `commit.gpgSign` supplies the default, command-line options\noverride it, and the last command-line option wins.\n\nThe signature records the attestation of the current committer to the\nrewritten commit while retaining the original author identity; it does\nnot claim authorship of commits written by somebody else.\n\nChanges since v2:\n\n - Shorten the commit messages based on review feedback\n - Rename the history implementation commit from `builtin/history` to\n   `history`\n - Fix the continuation-backslash formatting in `OPT_HISTORY_GPG_SIGN`\n\nSouma (2):\n  replay: allow callers to sign commits\n  history: sign rewritten commits\n\n Documentation/git-history.adoc | 16 +++++--\n builtin/history.c              | 84 ++++++++++++++++++++++++++--------\n replay.c                       | 13 ++++--\n replay.h                       |  6 +++\n t/t3451-history-reword.sh      | 63 +++++++++++++++++++++++++\n t/t3452-history-split.sh       | 44 ++++++++++++++++++\n t/t3453-history-fixup.sh       | 39 ++++++++++++++++\n t/t3454-history-drop.sh        | 50 ++++++++++++++++++++\n 8 files changed, 286 insertions(+), 29 deletions(-)\n\nRange-diff against v2:\n1:  3f4dc0b982 ! 1:  ca35b0acaa replay: allow callers to sign commits\n    @@ Metadata\n      ## Commit message ##\n         replay: allow callers to sign commits\n\n    -    The replay machinery creates commits directly through\n    -    `commit_tree_extended()`, but callers cannot currently request\n    -    signatures. Commands that replay rewritten history consequently cannot\n    -    carry their signing policy through to descendant commits.\n    -\n    -    Add `sign_commit` to `replay_revisions_options` and thread it through\n    -    commit creation. `NULL` preserves the existing unsigned behavior, an\n    -    empty string selects the default signing key, and a non-empty string\n    -    selects an explicit key. Existing callers zero-initialize the options\n    -    structure, so their behavior is unchanged.\n    +    Add a signing-key option to replay_revisions_options and pass it to\n    +    commit_tree_extended() when creating replayed commits.\n\n         Signed-off-by: Souma <git@5ouma.me>\n\n    @@ replay.c: static struct commit *pick_regular_commit(struct repository *repo,\n     +\t\t\t\t\t  enum replay_empty_commit_action empty,\n     +\t\t\t\t\t  const char *sign_commit)\n      {\n    - \tstruct commit *base, *replayed_base;\n      \tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n    +\n     @@ replay.c: static struct commit *pick_regular_commit(struct repository *repo,\n      \t\t}\n      \t}\n\n     -\treturn create_commit(repo, result->tree, pickme, replayed_base, mode);\n     +\treturn create_commit(repo, result->tree, pickme, replayed_base, mode,\n    -+\t\t\t     sign_commit);\n    ++\t\t\t\t\t    sign_commit);\n      }\n\n      void replay_result_release(struct replay_result *result)\n     @@ replay.c: int replay_revisions(struct rev_info *revs,\n\n    - \t\tlast_commit = pick_regular_commit(revs->repo, commit, replayed_commits,\n    - \t\t\t\t\t\t  mode == REPLAY_MODE_REVERT ? last_commit : onto,\n    --\t\t\t\t\t\t  &merge_opt, &result, mode, opts->empty);\n    -+\t\t\t\t\t\t  &merge_opt, &result, mode, opts->empty,\n    -+\t\t\t\t\t\t  opts->sign_commit);\n    - \t\tif (!last_commit)\n    - \t\t\tbreak;\n    + \t\t\tlast_commit = pick_regular_commit(revs->repo, commit, base,\n    + \t\t\t\t\t\t\t  &merge_opt, &result,\n    +-\t\t\t\t\t\t\t  mode, opts->empty);\n    ++\t\t\t\t\t\t\t  mode, opts->empty,\n    ++\t\t\t\t\t\t\t  opts->sign_commit);\n    + \t\t}\n\n    + \t\tif (!last_commit)\n\n      ## replay.h ##\n     @@ replay.h: struct replay_revisions_options {\n2:  0e63c0b66a ! 2:  f0a1a88411 builtin/history: sign rewritten commits\n    @@ Metadata\n     Author: Souma <git@5ouma.me>\n\n      ## Commit message ##\n    -    builtin/history: sign rewritten commits\n    +    history: sign rewritten commits\n\n    -    The history commands create replacement commits directly instead of\n    -    using the sequencer or the commit porcelain. As a result, rewritten\n    -    commits ignore `commit.gpgSign` and cannot be signed on demand.\n    +    Add --gpg-sign/--no-gpg-sign support to git history and honor\n    +    commit.gpgSign when creating replacement commits. Thread the selected\n    +    signing key through direct rewrites and replayed descendants while\n    +    preserving the original author identity.\n\n    -    Read the signing configuration before parsing options so that it\n    -    establishes the default and later `-S`/`--gpg-sign` or `--no-gpg-sign`\n    -    options override it. Pass the selected key through direct rewrites and\n    -    the replay machinery.\n    -\n    -    Sign every newly created commit, including both halves of a split and\n    -    replayed descendants. Dropping the tip creates no replacement commit,\n    -    so there is nothing to sign. As with `rebase --gpg-sign`, the signature\n    -    records the attestation of the current committer to the rewritten\n    -    commit while retaining the original author identity; it does not claim\n    -    authorship of commits written by somebody else.\n    -\n    -    Document the behavior and add GPG-gated coverage for configuration,\n    -    command-line overrides, last-option-wins precedence, replayed\n    -    descendants, split commits, an explicit signing key, and the\n    -    no-new-commit drop case.\n    +    Cover configuration, command-line precedence, explicit keys, split commits,\n    +    and replayed descendants with GPG-gated tests.\n\n         Signed-off-by: Souma <git@5ouma.me>\n\n    @@ builtin/history.c: enum commit_tree_flags {\n     +\treturn git_default_config(var, value, ctx, NULL);\n     +}\n     +\n    -+#define OPT_HISTORY_GPG_SIGN(v) {                 \\\n    -+\t.type = OPTION_STRING,                    \\\n    -+\t.short_name = 'S',                        \\\n    -+\t.long_name = \"gpg-sign\",                  \\\n    -+\t.value = (v),                             \\\n    -+\t.argh = N_(\"key-id\"),                     \\\n    ++#define OPT_HISTORY_GPG_SIGN(v) { \\\n    ++\t.type = OPTION_STRING, \\\n    ++\t.short_name = 'S', \\\n    ++\t.long_name = \"gpg-sign\", \\\n    ++\t.value = (v), \\\n    ++\t.argh = N_(\"key-id\"), \\\n     +\t.help = N_(\"GPG-sign rewritten commits\"), \\\n    -+\t.flags = PARSE_OPT_OPTARG,                \\\n    -+\t.defval = (intptr_t)\"\",                   \\\n    ++\t.flags = PARSE_OPT_OPTARG, \\\n    ++\t.defval = (intptr_t)\"\", \\\n     +}\n     +\n      static int commit_tree_ext(struct repository *repo,\n--\n2.55.0\n\n"},{"id":"552624","messageId":"20260912160045.36064-2-git@5ouma.me","threadId":"65920","inReplyTo":"20260703145037.69832-1-git@5ouma.me","subject":"[PATCH v3 1/2] replay: allow callers to sign commits","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-09-12T16:00:44Z","receivedAt":"2026-09-12T16:00:59Z","isPatch":true,"body":"Add a signing-key option to replay_revisions_options and pass it to\ncommit_tree_extended() when creating replayed commits.\n\nSigned-off-by: Souma <git@5ouma.me>\n---\n replay.c | 13 ++++++++-----\n replay.h |  6 ++++++\n 2 files changed, 14 insertions(+), 5 deletions(-)\n\ndiff --git a/replay.c b/replay.c\nindex f415103023..3e8a70bce1 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -85,13 +85,13 @@ static struct commit *create_commit(struct repository *repo,\n \t\t\t\t    struct tree *tree,\n \t\t\t\t    struct commit *based_on,\n \t\t\t\t    struct commit *parent,\n-\t\t\t\t    enum replay_mode mode)\n+\t\t\t\t    enum replay_mode mode,\n+\t\t\t\t    const char *sign_commit)\n {\n \tstruct object_id ret;\n \tstruct object *obj = NULL;\n \tstruct commit_list *parents = NULL;\n \tchar *author = NULL;\n-\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n \tstruct commit_extra_header *extra = NULL;\n \tstruct strbuf msg = STRBUF_INIT;\n \tconst char *out_enc = get_commit_output_encoding();\n@@ -288,7 +288,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \t\t\t\t\t  struct merge_options *merge_opt,\n \t\t\t\t\t  struct merge_result *result,\n \t\t\t\t\t  enum replay_mode mode,\n-\t\t\t\t\t  enum replay_empty_commit_action empty)\n+\t\t\t\t\t  enum replay_empty_commit_action empty,\n+\t\t\t\t\t  const char *sign_commit)\n {\n \tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n \n@@ -361,7 +362,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \t\t}\n \t}\n \n-\treturn create_commit(repo, result->tree, pickme, replayed_base, mode);\n+\treturn create_commit(repo, result->tree, pickme, replayed_base, mode,\n+\t\t\t\t\t    sign_commit);\n }\n \n void replay_result_release(struct replay_result *result)\n@@ -481,7 +483,8 @@ int replay_revisions(struct rev_info *revs,\n \n \t\t\tlast_commit = pick_regular_commit(revs->repo, commit, base,\n \t\t\t\t\t\t\t  &merge_opt, &result,\n-\t\t\t\t\t\t\t  mode, opts->empty);\n+\t\t\t\t\t\t\t  mode, opts->empty,\n+\t\t\t\t\t\t\t  opts->sign_commit);\n \t\t}\n \n \t\tif (!last_commit)\ndiff --git a/replay.h b/replay.h\nindex 2c71afbfde..2eb7704b74 100644\n--- a/replay.h\n+++ b/replay.h\n@@ -57,6 +57,12 @@ struct replay_revisions_options {\n \t */\n \tint contained;\n \n+\t/*\n+\t * Key used to sign newly-created commits. An empty string requests the\n+\t * default configured signing key, and NULL disables signing.\n+\t */\n+\tconst char *sign_commit;\n+\n \t/*\n \t * Controls what to do when a replayed commit becomes empty.\n \t * Defaults to REPLAY_EMPTY_COMMIT_DROP.\n-- \n2.55.0\n\n"},{"id":"552625","messageId":"20260912160045.36064-3-git@5ouma.me","threadId":"65920","inReplyTo":"20260703145037.69832-1-git@5ouma.me","subject":"[PATCH v3 2/2] history: sign rewritten commits","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-09-12T16:00:45Z","receivedAt":"2026-09-12T16:01:01Z","isPatch":true,"body":"Add --gpg-sign/--no-gpg-sign support to git history and honor\ncommit.gpgSign when creating replacement commits. Thread the selected\nsigning key through direct rewrites and replayed descendants while\npreserving the original author identity.\n\nCover configuration, command-line precedence, explicit keys, split commits,\nand replayed descendants with GPG-gated tests.\n\nSigned-off-by: Souma <git@5ouma.me>\n---\n Documentation/git-history.adoc | 16 +++++--\n builtin/history.c              | 84 ++++++++++++++++++++++++++--------\n t/t3451-history-reword.sh      | 63 +++++++++++++++++++++++++\n t/t3452-history-split.sh       | 44 ++++++++++++++++++\n t/t3453-history-fixup.sh       | 39 ++++++++++++++++\n t/t3454-history-drop.sh        | 50 ++++++++++++++++++++\n 6 files changed, 272 insertions(+), 24 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 28b477cd37..8345cced4c 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,10 +8,10 @@ git-history - EXPERIMENTAL: Rewrite history\n SYNOPSIS\n --------\n [synopsis]\n-git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]\n-git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]\n-git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\n-git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\n+git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\n+git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\n+git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]\n+git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]\n \n DESCRIPTION\n -----------\n@@ -125,6 +125,14 @@ OPTIONS\n `--reedit-message`::\n \tOpen an editor to modify the target commit's message.\n \n+`-S[<key-id>]`::\n+`--gpg-sign[=<key-id>]`::\n+`--no-gpg-sign`::\n+\tGPG-sign rewritten commits. The _<key-id>_ argument is optional and\n+\tdefaults to the committer identity; if specified, it must be stuck to\n+\tthe option without a space. `--no-gpg-sign` is useful to countermand\n+\tboth `commit.gpgSign` configuration and earlier `--gpg-sign`.\n+\n `--empty=(drop|keep|abort)`::\n \tControl what happens when a commit becomes empty as a result of the\n \tfixup. This can happen in two situations:\ndiff --git a/builtin/history.c b/builtin/history.c\nindex 000155ad9c..07b7c73c89 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -27,13 +27,13 @@\n #include \"wt-status.h\"\n \n #define GIT_HISTORY_DROP_USAGE \\\n-\tN_(\"git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]\")\n+\tN_(\"git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\")\n #define GIT_HISTORY_FIXUP_USAGE \\\n-\tN_(\"git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]\")\n+\tN_(\"git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\")\n #define GIT_HISTORY_REWORD_USAGE \\\n-\tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\")\n+\tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]\")\n #define GIT_HISTORY_SPLIT_USAGE \\\n-\tN_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\")\n+\tN_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]\")\n \n static void change_data_free(void *util, const char *str UNUSED)\n {\n@@ -105,12 +105,37 @@ enum commit_tree_flags {\n \tCOMMIT_TREE_EDIT_MESSAGE = (1 << 0),\n };\n \n+static int history_config(const char *var, const char *value,\n+\t\t\t  const struct config_context *ctx, void *data)\n+{\n+\tconst char **sign_commit = data;\n+\n+\tif (!strcmp(var, \"commit.gpgsign\")) {\n+\t\t*sign_commit = git_config_bool(var, value) ? \"\" : NULL;\n+\t\treturn 0;\n+\t}\n+\n+\treturn git_default_config(var, value, ctx, NULL);\n+}\n+\n+#define OPT_HISTORY_GPG_SIGN(v) { \\\n+\t.type = OPTION_STRING, \\\n+\t.short_name = 'S', \\\n+\t.long_name = \"gpg-sign\", \\\n+\t.value = (v), \\\n+\t.argh = N_(\"key-id\"), \\\n+\t.help = N_(\"GPG-sign rewritten commits\"), \\\n+\t.flags = PARSE_OPT_OPTARG, \\\n+\t.defval = (intptr_t)\"\", \\\n+}\n+\n static int commit_tree_ext(struct repository *repo,\n \t\t\t   const char *action,\n \t\t\t   struct commit *commit_with_message,\n \t\t\t   const struct commit_list *parents,\n \t\t\t   const struct object_id *old_tree,\n \t\t\t   const struct object_id *new_tree,\n+\t\t\t   const char *sign_commit,\n \t\t\t   struct commit **out,\n \t\t\t   enum commit_tree_flags flags)\n {\n@@ -151,7 +176,7 @@ static int commit_tree_ext(struct repository *repo,\n \n \tret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree,\n \t\t\t\t   parents, &rewritten_commit_oid, original_author,\n-\t\t\t\t   NULL, NULL, original_extra_headers);\n+\t\t\t\t   NULL, sign_commit, original_extra_headers);\n \tif (ret < 0)\n \t\tgoto out;\n \n@@ -167,6 +192,7 @@ static int commit_tree_ext(struct repository *repo,\n static int commit_tree_with_edited_message(struct repository *repo,\n \t\t\t\t\t   const char *action,\n \t\t\t\t\t   struct commit *original,\n+\t\t\t\t\t   const char *sign_commit,\n \t\t\t\t\t   struct commit **out)\n {\n \tstruct object_id parent_tree_oid;\n@@ -188,7 +214,8 @@ static int commit_tree_with_edited_message(struct repository *repo,\n \t}\n \n \treturn commit_tree_ext(repo, action, original, original->parents,\n-\t\t\t       &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);\n+\t\t\t       &parent_tree_oid, tree_oid, sign_commit, out,\n+\t\t\t       COMMIT_TREE_EDIT_MESSAGE);\n }\n \n enum ref_action {\n@@ -344,12 +371,14 @@ static int compute_pending_ref_updates(struct rev_info *revs,\n \t\t\t\t       enum ref_action action,\n \t\t\t\t       struct commit *original,\n \t\t\t\t       struct commit *rewritten,\n+\t\t\t\t       const char *sign_commit,\n \t\t\t\t       enum replay_empty_commit_action empty,\n \t\t\t\t       struct replay_result *result)\n {\n \tconst struct name_decoration *decoration;\n \tstruct replay_revisions_options opts = {\n \t\t.empty = empty,\n+\t\t.sign_commit = sign_commit,\n \t};\n \tchar hex[GIT_MAX_HEXSZ + 1];\n \tbool detached_head;\n@@ -454,13 +483,14 @@ static int handle_reference_updates(struct rev_info *revs,\n \t\t\t\t    struct commit *rewritten,\n \t\t\t\t    const char *reflog_msg,\n \t\t\t\t    int dry_run,\n+\t\t\t\t    const char *sign_commit,\n \t\t\t\t    enum replay_empty_commit_action empty)\n {\n \tstruct replay_result result = { 0 };\n \tint ret;\n \n \tret = compute_pending_ref_updates(revs, action, original, rewritten,\n-\t\t\t\t\t  empty, &result);\n+\t\t\t\t\t  sign_commit, empty, &result);\n \tif (ret)\n \t\tgoto out;\n \n@@ -522,6 +552,7 @@ static int cmd_history_fixup(int argc,\n \tenum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;\n \tenum ref_action action = REF_ACTION_DEFAULT;\n \tenum commit_tree_flags flags = 0;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -535,6 +566,7 @@ static int cmd_history_fixup(int argc,\n \t\tOPT_CALLBACK_F(0, \"empty\", &empty, \"(drop|keep|abort)\",\n \t\t\t       N_(\"how to handle commits that become empty\"),\n \t\t\t       PARSE_OPT_NONEG, parse_opt_empty),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct merge_result merge_result = { 0 };\n@@ -546,12 +578,12 @@ static int cmd_history_fixup(int argc,\n \tbool skip_commit = false;\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc != 1) {\n \t\tret = error(_(\"command expects a single revision\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -676,7 +708,7 @@ static int cmd_history_fixup(int argc,\n \tif (!skip_commit) {\n \t\tret = commit_tree_ext(repo, \"fixup\", original, original->parents,\n \t\t\t\t      &original_tree->object.oid, &merge_result.tree->object.oid,\n-\t\t\t\t      &rewritten, flags);\n+\t\t\t\t      sign_commit, &rewritten, flags);\n \t\tif (ret < 0) {\n \t\t\tret = error(_(\"failed writing fixed-up commit\"));\n \t\t\tgoto out;\n@@ -686,7 +718,7 @@ static int cmd_history_fixup(int argc,\n \tstrbuf_addf(&reflog_msg, \"fixup: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf, dry_run, empty);\n+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit, empty);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\n@@ -711,6 +743,7 @@ static int cmd_history_reword(int argc,\n \t\tNULL,\n \t};\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -718,6 +751,7 @@ static int cmd_history_reword(int argc,\n \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n \t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n \t\t\t N_(\"perform a dry-run without updating any refs\")),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n@@ -725,12 +759,12 @@ static int cmd_history_reword(int argc,\n \tstruct rev_info revs = { 0 };\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc != 1) {\n \t\tret = error(_(\"command expects a single revision\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -745,7 +779,8 @@ static int cmd_history_reword(int argc,\n \tif (ret)\n \t\tgoto out;\n \n-\tret = commit_tree_with_edited_message(repo, \"reworded\", original, &rewritten);\n+\tret = commit_tree_with_edited_message(repo, \"reworded\", original,\n+\t\t\t\t\t      sign_commit, &rewritten);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing reworded commit\"));\n \t\tgoto out;\n@@ -754,7 +789,8 @@ static int cmd_history_reword(int argc,\n \tstrbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);\n+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit,\n+\t\t\t\t       REPLAY_EMPTY_COMMIT_ABORT);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\n@@ -816,6 +852,7 @@ static int write_ondisk_index(struct repository *repo,\n static int split_commit(struct repository *repo,\n \t\t\tstruct commit *original,\n \t\t\tstruct pathspec *pathspec,\n+\t\t\tconst char *sign_commit,\n \t\t\tstruct commit **out)\n {\n \tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n@@ -893,7 +930,8 @@ static int split_commit(struct repository *repo,\n \t * that shall be diffed against is the parent of the original commit.\n \t */\n \tret = commit_tree_ext(repo, \"split-out\", original, original->parents, &parent_tree_oid,\n-\t\t\t      &split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);\n+\t\t\t      &split_tree->object.oid, sign_commit, &first_commit,\n+\t\t\t      COMMIT_TREE_EDIT_MESSAGE);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing first commit\"));\n \t\tgoto out;\n@@ -910,7 +948,8 @@ static int split_commit(struct repository *repo,\n \tnew_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;\n \n \tret = commit_tree_ext(repo, \"split-out\", original, parents, old_tree_oid,\n-\t\t\t      new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);\n+\t\t\t      new_tree_oid, sign_commit, &second_commit,\n+\t\t\t      COMMIT_TREE_EDIT_MESSAGE);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing second commit\"));\n \t\tgoto out;\n@@ -938,6 +977,7 @@ static int cmd_history_split(int argc,\n \t\tNULL,\n \t};\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -945,6 +985,7 @@ static int cmd_history_split(int argc,\n \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n \t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n \t\t\t N_(\"perform a dry-run without updating any refs\")),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct commit *original, *rewritten = NULL;\n@@ -953,12 +994,12 @@ static int cmd_history_split(int argc,\n \tstruct rev_info revs = { 0 };\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc < 1) {\n \t\tret = error(_(\"command expects a committish\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -984,14 +1025,15 @@ static int cmd_history_split(int argc,\n \t\tgoto out;\n \t}\n \n-\tret = split_commit(repo, original, &pathspec, &rewritten);\n+\tret = split_commit(repo, original, &pathspec, sign_commit, &rewritten);\n \tif (ret < 0)\n \t\tgoto out;\n \n \tstrbuf_addf(&reflog_msg, \"split: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);\n+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit,\n+\t\t\t\t       REPLAY_EMPTY_COMMIT_ABORT);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\n@@ -1081,6 +1123,7 @@ static int cmd_history_drop(int argc,\n \t};\n \tenum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -1091,6 +1134,7 @@ static int cmd_history_drop(int argc,\n \t\tOPT_CALLBACK_F(0, \"empty\", &empty, \"(drop|keep|abort)\",\n \t\t\t       N_(\"how to handle descendants that become empty\"),\n \t\t\t       PARSE_OPT_NONEG, parse_opt_empty),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n@@ -1101,12 +1145,12 @@ static int cmd_history_drop(int argc,\n \tbool head_moves = false;\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc != 1) {\n \t\tret = error(_(\"command expects a single revision\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -1134,7 +1178,7 @@ static int cmd_history_drop(int argc,\n \trewritten = original->parents->item;\n \n \tret = compute_pending_ref_updates(&revs, action, original, rewritten,\n-\t\t\t\t\t  empty, &result);\n+\t\t\t\t\t  sign_commit, empty, &result);\n \tif (ret) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex de7b357685..6dbe2143d3 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -4,6 +4,7 @@ test_description='tests for git-history reword subcommand'\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY/lib-log-graph.sh\"\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n reword_with_message () {\n \tcat >message &&\n@@ -26,6 +27,37 @@ expect_log () {\n \ttest_cmp expect actual\n }\n \n+test_reword_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"reword $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit first &&\n+\t\t\ttest_commit second &&\n+\t\t\ttest_commit third &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\treword_with_message $* HEAD~ <<-EOF &&\n+\t\t\tsecond reworded\n+\t\t\tEOF\n+\n+\t\t\t$must_fail git verify-commit HEAD~ &&\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'can reword tip of a branch' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -77,6 +109,37 @@ test_expect_success 'can reword commit in the middle' '\n \t)\n '\n \n+test_reword_gpg_sign ! false\n+test_reword_gpg_sign   true\n+test_reword_gpg_sign   false --gpg-sign\n+test_reword_gpg_sign ! true  --no-gpg-sign\n+test_reword_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_reword_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n+test_expect_success GPG 'reword uses an explicit signing key for rewritten history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\treword_with_message -SB7227189 HEAD~ <<-EOF &&\n+\t\tsecond reworded\n+\t\tEOF\n+\n+\t\tgit verify-commit HEAD~ &&\n+\t\tgit verify-commit HEAD &&\n+\t\tgit log -2 --format=%GK >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\t65A0EEA02E30CAD7\n+\t\t65A0EEA02E30CAD7\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n test_expect_success 'can reword commit in the middle even on detached head' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\ndiff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh\nindex 8ed0cebb50..e96f492cc6 100755\n--- a/t/t3452-history-split.sh\n+++ b/t/t3452-history-split.sh\n@@ -4,6 +4,7 @@ test_description='tests for git-history split subcommand'\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY/lib-log-graph.sh\"\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n # The fake editor takes multiple arguments, each of which represents a commit\n # message. Subsequent invocations of the editor will then yield those messages\n@@ -36,6 +37,42 @@ expect_tree_entries () {\n \ttest_cmp expect actual\n }\n \n+test_split_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"split $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit initial &&\n+\t\t\ttouch bar foo &&\n+\t\t\tgit add . &&\n+\t\t\tgit commit -m split-me &&\n+\t\t\ttest_commit tip &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\tset_fake_editor 'first' 'second' &&\n+\t\t\tgit history split $* HEAD~ <<-EOF &&\n+\t\t\ty\n+\t\t\tn\n+\t\t\tEOF\n+\n+\t\t\t$must_fail git verify-commit HEAD~2 &&\n+\t\t\t$must_fail git verify-commit HEAD~ &&\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'refuses to work with merge commits' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -141,6 +178,13 @@ test_expect_success 'can split up tip commit' '\n \t)\n '\n \n+test_split_gpg_sign ! false\n+test_split_gpg_sign   true\n+test_split_gpg_sign   false --gpg-sign\n+test_split_gpg_sign ! true  --no-gpg-sign\n+test_split_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_split_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n test_expect_success 'can split up root commit' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\ndiff --git a/t/t3453-history-fixup.sh b/t/t3453-history-fixup.sh\nindex 868298e248..cd20a23115 100755\n--- a/t/t3453-history-fixup.sh\n+++ b/t/t3453-history-fixup.sh\n@@ -3,6 +3,7 @@\n test_description='tests for git-history fixup subcommand'\n \n . ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n fixup_with_message () {\n \tcat >message &&\n@@ -21,6 +22,37 @@ expect_changes () {\n \ttest_cmp expect actual\n }\n \n+test_fixup_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"fixup $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit first &&\n+\t\t\ttest_commit second &&\n+\t\t\ttest_commit third &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\techo fix >>second.t &&\n+\t\t\tgit add second.t &&\n+\t\t\tgit history fixup $* HEAD~ &&\n+\n+\t\t\t$must_fail git verify-commit HEAD~ &&\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'errors on missing commit argument' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -229,6 +261,13 @@ test_expect_success 'preserves commit message and authorship' '\n \t)\n '\n \n+test_fixup_gpg_sign ! false\n+test_fixup_gpg_sign   true\n+test_fixup_gpg_sign   false --gpg-sign\n+test_fixup_gpg_sign ! true  --no-gpg-sign\n+test_fixup_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_fixup_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n test_expect_success 'updates all descendant branches by default' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo --initial-branch=main &&\ndiff --git a/t/t3454-history-drop.sh b/t/t3454-history-drop.sh\nindex 68a86d1e37..5b21078a7e 100755\n--- a/t/t3454-history-drop.sh\n+++ b/t/t3454-history-drop.sh\n@@ -4,6 +4,7 @@ test_description='tests for git-history drop subcommand'\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY/lib-log-graph.sh\"\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n expect_graph () {\n \tcat >expect &&\n@@ -16,6 +17,34 @@ expect_log () {\n \ttest_cmp expect actual\n }\n \n+test_drop_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"drop $* with commit.gpgsign=$conf $will sign replayed descendants\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit first &&\n+\t\t\ttest_commit second &&\n+\t\t\ttest_commit third &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\tgit history drop $* HEAD~ &&\n+\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'errors on missing commit argument' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -88,6 +117,27 @@ test_expect_success 'drops a commit in the middle and replays descendants' '\n \t)\n '\n \n+test_drop_gpg_sign ! false\n+test_drop_gpg_sign   true\n+test_drop_gpg_sign   false --gpg-sign\n+test_drop_gpg_sign ! true  --no-gpg-sign\n+test_drop_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_drop_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n+test_expect_success GPG 'drop has no commit to sign when dropping the tip' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\n+\t\tgit history drop --gpg-sign HEAD &&\n+\n+\t\ttest_must_fail git verify-commit HEAD\n+\t)\n+'\n+\n test_expect_success 'drops the HEAD commit' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n-- \n2.55.0\n\n"},{"id":"553412","messageId":"aroX94CD_kOyLnuW@pks.im","threadId":"65920","inReplyTo":"20260912160045.36064-3-git@5ouma.me","subject":"Re: [PATCH v3 2/2] history: sign rewritten commits","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-09-28T07:32:07Z","receivedAt":"2026-09-28T07:32:14Z","isPatch":true,"body":"On Sun, Sep 13, 2026 at 01:00:45AM +0900, Souma wrote:\n> Add --gpg-sign/--no-gpg-sign support to git history and honor\n> commit.gpgSign when creating replacement commits. Thread the selected\n> signing key through direct rewrites and replayed descendants while\n> preserving the original author identity.\n> \n> Cover configuration, command-line precedence, explicit keys, split commits,\n> and replayed descendants with GPG-gated tests.\n\nThis is much shorter now, which is good. One question to ask yourself\nthough is whether there's any subtleties in the changes you perform that\nmight want to be explained.\n\nOne such subtlety for example is that you reorder the calls to\n`repo_config()`. It's obvious to me, but it may not be obvious to every\nreviewer why you do that. Pointing out and explaining details like this\nin a sentence or two is useful context.\n\nOther than these nits about the commit message I'm happy with this\nseries as-is. I won't insist on a reroll, but wouldn't mind if you did.\nThanks!\n\nPatrick\n"},{"id":"553414","messageId":"aroaBm1G2OjO2TiB@pks.im","threadId":"65920","inReplyTo":"20260912160045.36064-2-git@5ouma.me","subject":"Re: [PATCH v3 1/2] replay: allow callers to sign commits","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-09-28T07:40:54Z","receivedAt":"2026-09-28T07:41:00Z","isPatch":true,"body":"On Sun, Sep 13, 2026 at 01:00:44AM +0900, Souma wrote:\n> Add a signing-key option to replay_revisions_options and pass it to\n> commit_tree_extended() when creating replayed commits.\n\nThis is mostly sufficient. One bit of information that could be useful\nto the reviewer is that the infra is not used anywhere yet. But that\ndoes not warrant a reroll, as the patch looks good to me otherwise.\n\nPatrick\n"},{"id":"553485","messageId":"xmqqtsn9o1yj.fsf@gitster.g","threadId":"65920","inReplyTo":"aroX94CD_kOyLnuW@pks.im","subject":"Re: [PATCH v3 2/2] history: sign rewritten commits","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-28T15:00:36Z","receivedAt":"2026-09-28T15:00:39Z","isPatch":true,"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Sun, Sep 13, 2026 at 01:00:45AM +0900, Souma wrote:\n>> Add --gpg-sign/--no-gpg-sign support to git history and honor\n>> commit.gpgSign when creating replacement commits. Thread the selected\n>> signing key through direct rewrites and replayed descendants while\n>> preserving the original author identity.\n>> \n>> Cover configuration, command-line precedence, explicit keys, split commits,\n>> and replayed descendants with GPG-gated tests.\n>\n> This is much shorter now, which is good. One question to ask yourself\n> though is whether there's any subtleties in the changes you perform that\n> might want to be explained.\n>\n> One such subtlety for example is that you reorder the calls to\n> `repo_config()`. It's obvious to me, but it may not be obvious to every\n> reviewer why you do that. Pointing out and explaining details like this\n> in a sentence or two is useful context.\n\nThanks for pointing this out.  It encouraged me to take a peek into\nthe area in the patch ;-).\n\n> Other than these nits about the commit message I'm happy with this\n> series as-is. I won't insist on a reroll, but wouldn't mind if you did.\n> Thanks!\n\nThanks for writing, and thanks for reviewing.\n"},{"id":"553511","messageId":"xmqq8q4lmco7.fsf@gitster.g","threadId":"65920","inReplyTo":"xmqqtsn9o1yj.fsf@gitster.g","subject":"Re: [PATCH v3 2/2] history: sign rewritten commits","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-28T18:52:08Z","receivedAt":"2026-09-28T18:52:22Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n>> On Sun, Sep 13, 2026 at 01:00:45AM +0900, Souma wrote:\n> \n>> One such subtlety for example is that you reorder the calls to\n>> `repo_config()`. It's obvious to me, but it may not be obvious to every\n>> reviewer why you do that. Pointing out and explaining details like this\n>> in a sentence or two is useful context.\n>\n> Thanks for pointing this out.  It encouraged me to take a peek into\n> the area in the patch ;-).\n>\n>> Other than these nits about the commit message I'm happy with this\n>> series as-is. I won't insist on a reroll, but wouldn't mind if you did.\n>> Thanks!\n>\n> Thanks for writing, and thanks for reviewing.\n\nWhile we are on the topic of the proposed commit log message, is Souma\na real name or a handle?  Documentation/SubmittingPatches:[[dco]]\ndescribes a procedure with legal ramifications, and that is where\nDocumentation/SubmittingPatches:[[real-name]] comes into the picture,\nso I have to ask.\n\nThanks.\n"},{"id":"553530","messageId":"f2dd98d5-4bab-4692-8f3d-313de4b241f8@app.beta.fastmail.com","threadId":"65920","inReplyTo":"xmqq8q4lmco7.fsf@gitster.g","subject":"Re: [PATCH v3 2/2] history: sign rewritten commits","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-09-28T23:39:40Z","receivedAt":"2026-09-28T23:41:54Z","isPatch":true,"body":"Hi,\nThank you for reviewing my patches. I really appreciate your review and feedback. I’ll update the commit messages accordingly.\n\nRegarding the name in the Signed-off-by line, Souma is my legal first name, not a handle or pseudonym. So the name I’m using there is my real name.\n\nThank you again for your time and review.\n\nBest regards,\nSouma\n"},{"id":"553962","messageId":"20261002132718.3830-1-git@5ouma.me","threadId":"65920","inReplyTo":"20260703145037.69832-1-git@5ouma.me","subject":"[PATCH v4 0/2] history: sign rewritten commits","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-10-02T13:27:16Z","receivedAt":"2026-10-02T13:27:29Z","isPatch":true,"body":"History rewriting creates commits through two paths: the history commands\nwrite replacement commits directly, while the replay machinery recreates\ndescendants above the rewritten range. Neither path currently honors\n`commit.gpgSign` or an explicit signing request, so rewriting signed history\ncan leave the resulting commits unsigned.\n\nAdd a signing-key option to the replay API, then have the history commands\npass the selected signer through both paths. Expose the standard\n`-S`/`--gpg-sign[=<key-id>]` and `--no-gpg-sign` options for `drop`, `fixup`,\n`reword`, `split`, and `squash`. This applies one signing policy to every\ncommit created by the rewrite, including both commits from `split`, the\ncommit from `squash`, and replayed descendants.\n\nThe behavior follows rebase, cherry-pick, and revert:\n`commit.gpgSign` supplies the default, command-line options override it, and\nthe last command-line option wins. The signature attests the current\ncommitter's rewrite while preserving the original author identity.\n\nChanges since v3:\n\n - Add signing support to `git history squash`, including its synopsis,\n   configuration and command-line behavior, and replayed descendants\n - Add GPG-gated squash tests for configuration, option precedence,\n   explicit keys, the squashed commit, and replayed descendants\n - Document the signing options for the squash subcommand\n - Clarify the commit messages based on review feedback, including why\n   configuration is loaded before option parsing and that the replay\n   infrastructure is consumed by the follow-up history change\n\nSouma (2):\n  replay: allow callers to sign commits\n  history: sign rewritten commits\n\n Documentation/git-history.adoc | 18 +++++--\n builtin/history.c              | 96 +++++++++++++++++++++++++---------\n replay.c                       | 13 +++--\n replay.h                       |  6 +++\n t/t3451-history-reword.sh      | 63 ++++++++++++++++++++++\n t/t3452-history-split.sh       | 44 ++++++++++++++++\n t/t3453-history-fixup.sh       | 39 ++++++++++++++\n t/t3454-history-drop.sh        | 50 ++++++++++++++++++\n t/t3455-history-squash.sh      | 61 +++++++++++++++++++++\n 9 files changed, 356 insertions(+), 34 deletions(-)\n\nRange-diff against v3:\n1:  ca35b0acaa ! 1:  d45cce8e25 replay: allow callers to sign commits\n    @@ Commit message\n         Add a signing-key option to replay_revisions_options and pass it to\n         commit_tree_extended() when creating replayed commits.\n     \n    +    This provides the replay infrastructure for history commands to sign\n    +    replayed descendants.\n    +\n         Signed-off-by: Souma <git@5ouma.me>\n     \n      ## replay.c ##\n2:  f0a1a88411 ! 2:  8b4766fc0e history: sign rewritten commits\n    @@ Commit message\n         signing key through direct rewrites and replayed descendants while\n         preserving the original author identity.\n     \n    -    Cover configuration, command-line precedence, explicit keys, split commits,\n    -    and replayed descendants with GPG-gated tests.\n    +    Load history configuration before parsing command-line options so\n    +    command-line signing options override commit.gpgSign.\n    +\n    +    Cover configuration, command-line precedence, explicit keys, split\n    +    commits, and replayed descendants with GPG-gated tests.\n     \n         Signed-off-by: Souma <git@5ouma.me>\n     \n    @@ Documentation/git-history.adoc: git-history - EXPERIMENTAL: Rewrite history\n     -git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]\n     -git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\n     -git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\n    +-git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] <revision-range>\n     +git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\n     +git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\n     +git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]\n     +git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]\n    ++git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] [--[no-]gpg-sign[=<key-id>]] <revision-range>\n      \n      DESCRIPTION\n      -----------\n    @@ builtin/history.c\n      #define GIT_HISTORY_SPLIT_USAGE \\\n     -\tN_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\")\n     +\tN_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]\")\n    + #define GIT_HISTORY_SQUASH_USAGE \\\n    +-\tN_(\"git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] <revision-range>\")\n    ++\tN_(\"git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] [--[no-]gpg-sign[=<key-id>]] <revision-range>\")\n      \n      static void change_data_free(void *util, const char *str UNUSED)\n      {\n    @@ builtin/history.c: enum commit_tree_flags {\n      static int commit_tree_ext(struct repository *repo,\n      \t\t\t   const char *action,\n      \t\t\t   struct commit *commit_with_message,\n    +@@ builtin/history.c: static int commit_tree_ext(struct repository *repo,\n      \t\t\t   const struct commit_list *parents,\n      \t\t\t   const struct object_id *old_tree,\n      \t\t\t   const struct object_id *new_tree,\n    @@ builtin/history.c: static int commit_tree_ext(struct repository *repo,\n      \tif (ret < 0)\n      \t\tgoto out;\n      \n    -@@ builtin/history.c: static int commit_tree_ext(struct repository *repo,\n    +@@ builtin/history.c: static int first_parent_tree_oid(struct repository *repo,\n      static int commit_tree_with_edited_message(struct repository *repo,\n      \t\t\t\t\t   const char *action,\n      \t\t\t\t\t   struct commit *original,\n    @@ builtin/history.c: static int commit_tree_ext(struct repository *repo,\n      {\n      \tstruct object_id parent_tree_oid;\n     @@ builtin/history.c: static int commit_tree_with_edited_message(struct repository *repo,\n    - \t}\n    + \t\treturn -1;\n      \n    - \treturn commit_tree_ext(repo, action, original, original->parents,\n    + \treturn commit_tree_ext(repo, action, original, NULL, original->parents,\n     -\t\t\t       &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);\n     +\t\t\t       &parent_tree_oid, tree_oid, sign_commit, out,\n     +\t\t\t       COMMIT_TREE_EDIT_MESSAGE);\n    @@ builtin/history.c: static int cmd_history_fixup(int argc,\n      \t\taction = REF_ACTION_BRANCHES;\n     @@ builtin/history.c: static int cmd_history_fixup(int argc,\n      \tif (!skip_commit) {\n    - \t\tret = commit_tree_ext(repo, \"fixup\", original, original->parents,\n    + \t\tret = commit_tree_ext(repo, \"fixup\", original, NULL, original->parents,\n      \t\t\t\t      &original_tree->object.oid, &merge_result.tree->object.oid,\n     -\t\t\t\t      &rewritten, flags);\n     +\t\t\t\t      sign_commit, &rewritten, flags);\n    @@ builtin/history.c: static int write_ondisk_index(struct repository *repo,\n      {\n      \tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n     @@ builtin/history.c: static int split_commit(struct repository *repo,\n    + \t * The first commit is constructed from the split-out tree. The base\n      \t * that shall be diffed against is the parent of the original commit.\n      \t */\n    - \tret = commit_tree_ext(repo, \"split-out\", original, original->parents, &parent_tree_oid,\n    +-\tret = commit_tree_ext(repo, \"split-out\", original, NULL, original->parents, &parent_tree_oid,\n     -\t\t\t      &split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);\n    -+\t\t\t      &split_tree->object.oid, sign_commit, &first_commit,\n    ++\tret = commit_tree_ext(repo, \"split-out\", original, NULL, original->parents,\n    ++\t\t\t      &parent_tree_oid, &split_tree->object.oid, sign_commit,\n    ++\t\t\t      &first_commit,\n     +\t\t\t      COMMIT_TREE_EDIT_MESSAGE);\n      \tif (ret < 0) {\n      \t\tret = error(_(\"failed writing first commit\"));\n    @@ builtin/history.c: static int split_commit(struct repository *repo,\n     @@ builtin/history.c: static int split_commit(struct repository *repo,\n      \tnew_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;\n      \n    - \tret = commit_tree_ext(repo, \"split-out\", original, parents, old_tree_oid,\n    + \tret = commit_tree_ext(repo, \"split-out\", original, NULL, parents, old_tree_oid,\n     -\t\t\t      new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);\n     +\t\t\t      new_tree_oid, sign_commit, &second_commit,\n     +\t\t\t      COMMIT_TREE_EDIT_MESSAGE);\n    @@ builtin/history.c: static int cmd_history_split(int argc,\n      \tif (ret < 0) {\n      \t\tret = error(_(\"failed replaying descendants\"));\n      \t\tgoto out;\n    +@@ builtin/history.c: static int cmd_history_squash(int argc,\n    + \t\tNULL,\n    + \t};\n    + \tenum ref_action action = REF_ACTION_DEFAULT;\n    ++\tconst char *sign_commit = NULL;\n    + \tint dry_run = 0;\n    + \tint edit = 1;\n    + \tstruct option options[] = {\n    +@@ builtin/history.c: static int cmd_history_squash(int argc,\n    + \t\t\t N_(\"perform a dry-run without updating any refs\")),\n    + \t\tOPT_BOOL('e', \"edit\", &edit,\n    + \t\t\t N_(\"edit the commit message\")),\n    ++\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n    + \t\tOPT_END(),\n    + \t};\n    + \tstruct strbuf reflog_msg = STRBUF_INIT;\n    +@@ builtin/history.c: static int cmd_history_squash(int argc,\n    + \tstruct rev_info revs = { 0 };\n    + \tint ret;\n    + \n    ++\trepo_config(repo, history_config, &sign_commit);\n    + \targc = parse_options(argc, argv, prefix, options, usage,\n    + \t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_ARGV0);\n    + \tif (argc < 2) {\n    + \t\tret = error(_(\"command expects a revision range\"));\n    + \t\tgoto out;\n    + \t}\n    +-\trepo_config(repo, git_default_config, NULL);\n    + \n    + \tif (action == REF_ACTION_DEFAULT)\n    + \t\taction = REF_ACTION_BRANCHES;\n    +@@ builtin/history.c: static int cmd_history_squash(int argc,\n    + \n    + \tret = commit_tree_ext(repo, \"squash\", oldest, message_template,\n    + \t\t\t      oldest->parents, base_tree_oid, tip_tree_oid,\n    +-\t\t\t      &rewritten,\n    ++\t\t\t      sign_commit, &rewritten,\n    + \t\t\t      edit ? COMMIT_TREE_EDIT_MESSAGE : 0);\n    + \tif (ret < 0) {\n    + \t\tret = error(_(\"failed writing squashed commit\"));\n    +@@ builtin/history.c: static int cmd_history_squash(int argc,\n    + \n    + \tret = handle_reference_updates(&revs, action, tip, rewritten,\n    + \t\t\t\t       reflog_msg.buf, dry_run,\n    ++\t\t\t\t       sign_commit,\n    + \t\t\t\t       REPLAY_EMPTY_COMMIT_ABORT);\n    + \tif (ret < 0) {\n    + \t\tret = error(_(\"failed replaying descendants\"));\n     @@ builtin/history.c: static int cmd_history_drop(int argc,\n      \t};\n      \tenum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;\n    @@ t/t3454-history-drop.sh: test_expect_success 'drops a commit in the middle and r\n      test_expect_success 'drops the HEAD commit' '\n      \ttest_when_finished \"rm -rf repo\" &&\n      \tgit init repo &&\n    +\n    + ## t/t3455-history-squash.sh ##\n    +@@\n    + test_description='tests for git-history squash subcommand'\n    + \n    + . ./test-lib.sh\n    ++. \"$TEST_DIRECTORY/lib-gpg.sh\"\n    + \n    + stage_file () {\n    + \tprintf \"%s\\n\" \"$1\" >file &&\n    +@@ t/t3455-history-squash.sh: check_commit_author () {\n    + \ttest_cmp expect actual\n    + }\n    + \n    ++test_squash_gpg_sign () {\n    ++\tmust_fail= will=will\n    ++\tif test \"x$1\" = \"x!\"\n    ++\tthen\n    ++\t\tmust_fail=test_must_fail\n    ++\t\twill=\"will not\"\n    ++\t\tshift\n    ++\tfi\n    ++\tconf=$1\n    ++\tshift\n    ++\n    ++\ttest_expect_success GPG \"squash $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n    ++\t\ttest_when_finished 'rm -rf repo' &&\n    ++\t\tgit init repo &&\n    ++\t\t(\n    ++\t\t\tcd repo &&\n    ++\t\t\ttest_commit first &&\n    ++\t\t\ttest_commit second &&\n    ++\t\t\ttest_commit third &&\n    ++\t\t\ttest_commit fourth &&\n    ++\n    ++\t\t\tgit config commit.gpgsign $conf &&\n    ++\t\t\tgit history squash --no-edit $* HEAD~3..HEAD~1 &&\n    ++\n    ++\t\t\t$must_fail git verify-commit HEAD~ &&\n    ++\t\t\t$must_fail git verify-commit HEAD\n    ++\t\t)\n    ++\t\"\n    ++}\n    ++\n    ++test_squash_gpg_sign ! false\n    ++test_squash_gpg_sign   true\n    ++test_squash_gpg_sign   false --gpg-sign\n    ++test_squash_gpg_sign ! true  --no-gpg-sign\n    ++test_squash_gpg_sign ! true  --gpg-sign --no-gpg-sign\n    ++test_squash_gpg_sign   false --no-gpg-sign --gpg-sign\n    ++\n    ++test_expect_success GPG 'squash uses an explicit signing key for rewritten history' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tgit init repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\ttest_commit first &&\n    ++\t\ttest_commit second &&\n    ++\t\ttest_commit third &&\n    ++\t\ttest_commit fourth &&\n    ++\n    ++\t\tgit history squash --no-edit -SB7227189 HEAD~3..HEAD~1 &&\n    ++\n    ++\t\tgit verify-commit HEAD~ &&\n    ++\t\tgit verify-commit HEAD &&\n    ++\t\tgit log -2 --format=%GK >actual &&\n    ++\t\tcat >expect <<-\\EOF &&\n    ++\t\t65A0EEA02E30CAD7\n    ++\t\t65A0EEA02E30CAD7\n    ++\t\tEOF\n    ++\t\ttest_cmp expect actual\n    ++\t)\n    ++'\n    ++\n    + test_expect_success 'setup linear history touching two files' '\n    + \ttest_commit base file a start &&\n    + \tGIT_AUTHOR_NAME=One GIT_AUTHOR_EMAIL=one@example.com \\\n-- \n2.56.0\n"},{"id":"553963","messageId":"20261002132718.3830-2-git@5ouma.me","threadId":"65920","inReplyTo":"20260703145037.69832-1-git@5ouma.me","subject":"[PATCH v4 1/2] replay: allow callers to sign commits","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-10-02T13:27:17Z","receivedAt":"2026-10-02T13:27:32Z","isPatch":true,"body":"Add a signing-key option to replay_revisions_options and pass it to\ncommit_tree_extended() when creating replayed commits.\n\nThis provides the replay infrastructure for history commands to sign\nreplayed descendants.\n\nSigned-off-by: Souma <git@5ouma.me>\n---\n replay.c | 13 ++++++++-----\n replay.h |  6 ++++++\n 2 files changed, 14 insertions(+), 5 deletions(-)\n\ndiff --git a/replay.c b/replay.c\nindex f415103023..3e8a70bce1 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -85,13 +85,13 @@ static struct commit *create_commit(struct repository *repo,\n \t\t\t\t    struct tree *tree,\n \t\t\t\t    struct commit *based_on,\n \t\t\t\t    struct commit *parent,\n-\t\t\t\t    enum replay_mode mode)\n+\t\t\t\t    enum replay_mode mode,\n+\t\t\t\t    const char *sign_commit)\n {\n \tstruct object_id ret;\n \tstruct object *obj = NULL;\n \tstruct commit_list *parents = NULL;\n \tchar *author = NULL;\n-\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n \tstruct commit_extra_header *extra = NULL;\n \tstruct strbuf msg = STRBUF_INIT;\n \tconst char *out_enc = get_commit_output_encoding();\n@@ -288,7 +288,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \t\t\t\t\t  struct merge_options *merge_opt,\n \t\t\t\t\t  struct merge_result *result,\n \t\t\t\t\t  enum replay_mode mode,\n-\t\t\t\t\t  enum replay_empty_commit_action empty)\n+\t\t\t\t\t  enum replay_empty_commit_action empty,\n+\t\t\t\t\t  const char *sign_commit)\n {\n \tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n \n@@ -361,7 +362,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \t\t}\n \t}\n \n-\treturn create_commit(repo, result->tree, pickme, replayed_base, mode);\n+\treturn create_commit(repo, result->tree, pickme, replayed_base, mode,\n+\t\t\t\t\t    sign_commit);\n }\n \n void replay_result_release(struct replay_result *result)\n@@ -481,7 +483,8 @@ int replay_revisions(struct rev_info *revs,\n \n \t\t\tlast_commit = pick_regular_commit(revs->repo, commit, base,\n \t\t\t\t\t\t\t  &merge_opt, &result,\n-\t\t\t\t\t\t\t  mode, opts->empty);\n+\t\t\t\t\t\t\t  mode, opts->empty,\n+\t\t\t\t\t\t\t  opts->sign_commit);\n \t\t}\n \n \t\tif (!last_commit)\ndiff --git a/replay.h b/replay.h\nindex 2c71afbfde..2eb7704b74 100644\n--- a/replay.h\n+++ b/replay.h\n@@ -57,6 +57,12 @@ struct replay_revisions_options {\n \t */\n \tint contained;\n \n+\t/*\n+\t * Key used to sign newly-created commits. An empty string requests the\n+\t * default configured signing key, and NULL disables signing.\n+\t */\n+\tconst char *sign_commit;\n+\n \t/*\n \t * Controls what to do when a replayed commit becomes empty.\n \t * Defaults to REPLAY_EMPTY_COMMIT_DROP.\n-- \n2.56.0\n\n"},{"id":"553964","messageId":"20261002132718.3830-3-git@5ouma.me","threadId":"65920","inReplyTo":"20260703145037.69832-1-git@5ouma.me","subject":"[PATCH v4 2/2] history: sign rewritten commits","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-10-02T13:27:18Z","receivedAt":"2026-10-02T13:27:33Z","isPatch":true,"body":"Add --gpg-sign/--no-gpg-sign support to git history and honor\ncommit.gpgSign when creating replacement commits. Thread the selected\nsigning key through direct rewrites and replayed descendants while\npreserving the original author identity.\n\nLoad history configuration before parsing command-line options so\ncommand-line signing options override commit.gpgSign.\n\nCover configuration, command-line precedence, explicit keys, split\ncommits, and replayed descendants with GPG-gated tests.\n\nSigned-off-by: Souma <git@5ouma.me>\n---\n Documentation/git-history.adoc | 18 +++++--\n builtin/history.c              | 96 +++++++++++++++++++++++++---------\n t/t3451-history-reword.sh      | 63 ++++++++++++++++++++++\n t/t3452-history-split.sh       | 44 ++++++++++++++++\n t/t3453-history-fixup.sh       | 39 ++++++++++++++\n t/t3454-history-drop.sh        | 50 ++++++++++++++++++\n t/t3455-history-squash.sh      | 61 +++++++++++++++++++++\n 7 files changed, 342 insertions(+), 29 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 2e2e31f521..1fcc30150a 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,11 +8,11 @@ git-history - EXPERIMENTAL: Rewrite history\n SYNOPSIS\n --------\n [synopsis]\n-git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]\n-git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]\n-git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\n-git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\n-git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] <revision-range>\n+git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\n+git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\n+git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]\n+git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]\n+git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] [--[no-]gpg-sign[=<key-id>]] <revision-range>\n \n DESCRIPTION\n -----------\n@@ -180,6 +180,14 @@ OPTIONS\n `--reedit-message`::\n \tOpen an editor to modify the target commit's message.\n \n+`-S[<key-id>]`::\n+`--gpg-sign[=<key-id>]`::\n+`--no-gpg-sign`::\n+\tGPG-sign rewritten commits. The _<key-id>_ argument is optional and\n+\tdefaults to the committer identity; if specified, it must be stuck to\n+\tthe option without a space. `--no-gpg-sign` is useful to countermand\n+\tboth `commit.gpgSign` configuration and earlier `--gpg-sign`.\n+\n `--empty=(drop|keep|abort)`::\n \tControl what happens when a commit becomes empty as a result of the\n \tfixup. This can happen in two situations:\ndiff --git a/builtin/history.c b/builtin/history.c\nindex 54cea3523f..43ba76f5a1 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -30,15 +30,15 @@\n #include \"wt-status.h\"\n \n #define GIT_HISTORY_DROP_USAGE \\\n-\tN_(\"git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]\")\n+\tN_(\"git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\")\n #define GIT_HISTORY_FIXUP_USAGE \\\n-\tN_(\"git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]\")\n+\tN_(\"git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\")\n #define GIT_HISTORY_REWORD_USAGE \\\n-\tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\")\n+\tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]\")\n #define GIT_HISTORY_SPLIT_USAGE \\\n-\tN_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\")\n+\tN_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]\")\n #define GIT_HISTORY_SQUASH_USAGE \\\n-\tN_(\"git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] <revision-range>\")\n+\tN_(\"git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] [--[no-]gpg-sign[=<key-id>]] <revision-range>\")\n \n static void change_data_free(void *util, const char *str UNUSED)\n {\n@@ -110,6 +110,30 @@ enum commit_tree_flags {\n \tCOMMIT_TREE_EDIT_MESSAGE = (1 << 0),\n };\n \n+static int history_config(const char *var, const char *value,\n+\t\t\t  const struct config_context *ctx, void *data)\n+{\n+\tconst char **sign_commit = data;\n+\n+\tif (!strcmp(var, \"commit.gpgsign\")) {\n+\t\t*sign_commit = git_config_bool(var, value) ? \"\" : NULL;\n+\t\treturn 0;\n+\t}\n+\n+\treturn git_default_config(var, value, ctx, NULL);\n+}\n+\n+#define OPT_HISTORY_GPG_SIGN(v) { \\\n+\t.type = OPTION_STRING, \\\n+\t.short_name = 'S', \\\n+\t.long_name = \"gpg-sign\", \\\n+\t.value = (v), \\\n+\t.argh = N_(\"key-id\"), \\\n+\t.help = N_(\"GPG-sign rewritten commits\"), \\\n+\t.flags = PARSE_OPT_OPTARG, \\\n+\t.defval = (intptr_t)\"\", \\\n+}\n+\n static int commit_tree_ext(struct repository *repo,\n \t\t\t   const char *action,\n \t\t\t   struct commit *commit_with_message,\n@@ -117,6 +141,7 @@ static int commit_tree_ext(struct repository *repo,\n \t\t\t   const struct commit_list *parents,\n \t\t\t   const struct object_id *old_tree,\n \t\t\t   const struct object_id *new_tree,\n+\t\t\t   const char *sign_commit,\n \t\t\t   struct commit **out,\n \t\t\t   enum commit_tree_flags flags)\n {\n@@ -160,7 +185,7 @@ static int commit_tree_ext(struct repository *repo,\n \n \tret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree,\n \t\t\t\t   parents, &rewritten_commit_oid, original_author,\n-\t\t\t\t   NULL, NULL, original_extra_headers);\n+\t\t\t\t   NULL, sign_commit, original_extra_headers);\n \tif (ret < 0)\n \t\tgoto out;\n \n@@ -196,6 +221,7 @@ static int first_parent_tree_oid(struct repository *repo,\n static int commit_tree_with_edited_message(struct repository *repo,\n \t\t\t\t\t   const char *action,\n \t\t\t\t\t   struct commit *original,\n+\t\t\t\t\t   const char *sign_commit,\n \t\t\t\t\t   struct commit **out)\n {\n \tstruct object_id parent_tree_oid;\n@@ -207,7 +233,8 @@ static int commit_tree_with_edited_message(struct repository *repo,\n \t\treturn -1;\n \n \treturn commit_tree_ext(repo, action, original, NULL, original->parents,\n-\t\t\t       &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);\n+\t\t\t       &parent_tree_oid, tree_oid, sign_commit, out,\n+\t\t\t       COMMIT_TREE_EDIT_MESSAGE);\n }\n \n enum ref_action {\n@@ -363,12 +390,14 @@ static int compute_pending_ref_updates(struct rev_info *revs,\n \t\t\t\t       enum ref_action action,\n \t\t\t\t       struct commit *original,\n \t\t\t\t       struct commit *rewritten,\n+\t\t\t\t       const char *sign_commit,\n \t\t\t\t       enum replay_empty_commit_action empty,\n \t\t\t\t       struct replay_result *result)\n {\n \tconst struct name_decoration *decoration;\n \tstruct replay_revisions_options opts = {\n \t\t.empty = empty,\n+\t\t.sign_commit = sign_commit,\n \t};\n \tchar hex[GIT_MAX_HEXSZ + 1];\n \tbool detached_head;\n@@ -473,13 +502,14 @@ static int handle_reference_updates(struct rev_info *revs,\n \t\t\t\t    struct commit *rewritten,\n \t\t\t\t    const char *reflog_msg,\n \t\t\t\t    int dry_run,\n+\t\t\t\t    const char *sign_commit,\n \t\t\t\t    enum replay_empty_commit_action empty)\n {\n \tstruct replay_result result = { 0 };\n \tint ret;\n \n \tret = compute_pending_ref_updates(revs, action, original, rewritten,\n-\t\t\t\t\t  empty, &result);\n+\t\t\t\t\t  sign_commit, empty, &result);\n \tif (ret)\n \t\tgoto out;\n \n@@ -533,6 +563,7 @@ static int cmd_history_fixup(int argc,\n \tenum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;\n \tenum ref_action action = REF_ACTION_DEFAULT;\n \tenum commit_tree_flags flags = 0;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -546,6 +577,7 @@ static int cmd_history_fixup(int argc,\n \t\tOPT_CALLBACK_F(0, \"empty\", &empty, \"(drop|keep|abort)\",\n \t\t\t       N_(\"how to handle commits that become empty\"),\n \t\t\t       PARSE_OPT_NONEG, parse_opt_empty),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct merge_result merge_result = { 0 };\n@@ -557,12 +589,12 @@ static int cmd_history_fixup(int argc,\n \tbool skip_commit = false;\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc != 1) {\n \t\tret = error(_(\"command expects a single revision\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -687,7 +719,7 @@ static int cmd_history_fixup(int argc,\n \tif (!skip_commit) {\n \t\tret = commit_tree_ext(repo, \"fixup\", original, NULL, original->parents,\n \t\t\t\t      &original_tree->object.oid, &merge_result.tree->object.oid,\n-\t\t\t\t      &rewritten, flags);\n+\t\t\t\t      sign_commit, &rewritten, flags);\n \t\tif (ret < 0) {\n \t\t\tret = error(_(\"failed writing fixed-up commit\"));\n \t\t\tgoto out;\n@@ -697,7 +729,7 @@ static int cmd_history_fixup(int argc,\n \tstrbuf_addf(&reflog_msg, \"fixup: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf, dry_run, empty);\n+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit, empty);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\n@@ -722,6 +754,7 @@ static int cmd_history_reword(int argc,\n \t\tNULL,\n \t};\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -729,6 +762,7 @@ static int cmd_history_reword(int argc,\n \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n \t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n \t\t\t N_(\"perform a dry-run without updating any refs\")),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n@@ -736,12 +770,12 @@ static int cmd_history_reword(int argc,\n \tstruct rev_info revs = { 0 };\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc != 1) {\n \t\tret = error(_(\"command expects a single revision\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -756,7 +790,8 @@ static int cmd_history_reword(int argc,\n \tif (ret)\n \t\tgoto out;\n \n-\tret = commit_tree_with_edited_message(repo, \"reworded\", original, &rewritten);\n+\tret = commit_tree_with_edited_message(repo, \"reworded\", original,\n+\t\t\t\t\t      sign_commit, &rewritten);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing reworded commit\"));\n \t\tgoto out;\n@@ -765,7 +800,8 @@ static int cmd_history_reword(int argc,\n \tstrbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);\n+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit,\n+\t\t\t\t       REPLAY_EMPTY_COMMIT_ABORT);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\n@@ -831,6 +867,7 @@ static int write_ondisk_index(struct repository *repo,\n static int split_commit(struct repository *repo,\n \t\t\tstruct commit *original,\n \t\t\tstruct pathspec *pathspec,\n+\t\t\tconst char *sign_commit,\n \t\t\tstruct commit **out)\n {\n \tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n@@ -900,8 +937,10 @@ static int split_commit(struct repository *repo,\n \t * The first commit is constructed from the split-out tree. The base\n \t * that shall be diffed against is the parent of the original commit.\n \t */\n-\tret = commit_tree_ext(repo, \"split-out\", original, NULL, original->parents, &parent_tree_oid,\n-\t\t\t      &split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);\n+\tret = commit_tree_ext(repo, \"split-out\", original, NULL, original->parents,\n+\t\t\t      &parent_tree_oid, &split_tree->object.oid, sign_commit,\n+\t\t\t      &first_commit,\n+\t\t\t      COMMIT_TREE_EDIT_MESSAGE);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing first commit\"));\n \t\tgoto out;\n@@ -918,7 +957,8 @@ static int split_commit(struct repository *repo,\n \tnew_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;\n \n \tret = commit_tree_ext(repo, \"split-out\", original, NULL, parents, old_tree_oid,\n-\t\t\t      new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);\n+\t\t\t      new_tree_oid, sign_commit, &second_commit,\n+\t\t\t      COMMIT_TREE_EDIT_MESSAGE);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing second commit\"));\n \t\tgoto out;\n@@ -946,6 +986,7 @@ static int cmd_history_split(int argc,\n \t\tNULL,\n \t};\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -953,6 +994,7 @@ static int cmd_history_split(int argc,\n \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n \t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n \t\t\t N_(\"perform a dry-run without updating any refs\")),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct commit *original, *rewritten = NULL;\n@@ -961,12 +1003,12 @@ static int cmd_history_split(int argc,\n \tstruct rev_info revs = { 0 };\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc < 1) {\n \t\tret = error(_(\"command expects a committish\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -992,14 +1034,15 @@ static int cmd_history_split(int argc,\n \t\tgoto out;\n \t}\n \n-\tret = split_commit(repo, original, &pathspec, &rewritten);\n+\tret = split_commit(repo, original, &pathspec, sign_commit, &rewritten);\n \tif (ret < 0)\n \t\tgoto out;\n \n \tstrbuf_addf(&reflog_msg, \"split: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);\n+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit,\n+\t\t\t\t       REPLAY_EMPTY_COMMIT_ABORT);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\n@@ -1579,6 +1622,7 @@ static int cmd_history_squash(int argc,\n \t\tNULL,\n \t};\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tint edit = 1;\n \tstruct option options[] = {\n@@ -1589,6 +1633,7 @@ static int cmd_history_squash(int argc,\n \t\t\t N_(\"perform a dry-run without updating any refs\")),\n \t\tOPT_BOOL('e', \"edit\", &edit,\n \t\t\t N_(\"edit the commit message\")),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n@@ -1598,13 +1643,13 @@ static int cmd_history_squash(int argc,\n \tstruct rev_info revs = { 0 };\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage,\n \t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_ARGV0);\n \tif (argc < 2) {\n \t\tret = error(_(\"command expects a revision range\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -1629,7 +1674,7 @@ static int cmd_history_squash(int argc,\n \n \tret = commit_tree_ext(repo, \"squash\", oldest, message_template,\n \t\t\t      oldest->parents, base_tree_oid, tip_tree_oid,\n-\t\t\t      &rewritten,\n+\t\t\t      sign_commit, &rewritten,\n \t\t\t      edit ? COMMIT_TREE_EDIT_MESSAGE : 0);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing squashed commit\"));\n@@ -1638,6 +1683,7 @@ static int cmd_history_squash(int argc,\n \n \tret = handle_reference_updates(&revs, action, tip, rewritten,\n \t\t\t\t       reflog_msg.buf, dry_run,\n+\t\t\t\t       sign_commit,\n \t\t\t\t       REPLAY_EMPTY_COMMIT_ABORT);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n@@ -1728,6 +1774,7 @@ static int cmd_history_drop(int argc,\n \t};\n \tenum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -1738,6 +1785,7 @@ static int cmd_history_drop(int argc,\n \t\tOPT_CALLBACK_F(0, \"empty\", &empty, \"(drop|keep|abort)\",\n \t\t\t       N_(\"how to handle descendants that become empty\"),\n \t\t\t       PARSE_OPT_NONEG, parse_opt_empty),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n@@ -1748,12 +1796,12 @@ static int cmd_history_drop(int argc,\n \tbool head_moves = false;\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc != 1) {\n \t\tret = error(_(\"command expects a single revision\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -1781,7 +1829,7 @@ static int cmd_history_drop(int argc,\n \trewritten = original->parents->item;\n \n \tret = compute_pending_ref_updates(&revs, action, original, rewritten,\n-\t\t\t\t\t  empty, &result);\n+\t\t\t\t\t  sign_commit, empty, &result);\n \tif (ret) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex de7b357685..6dbe2143d3 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -4,6 +4,7 @@ test_description='tests for git-history reword subcommand'\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY/lib-log-graph.sh\"\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n reword_with_message () {\n \tcat >message &&\n@@ -26,6 +27,37 @@ expect_log () {\n \ttest_cmp expect actual\n }\n \n+test_reword_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"reword $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit first &&\n+\t\t\ttest_commit second &&\n+\t\t\ttest_commit third &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\treword_with_message $* HEAD~ <<-EOF &&\n+\t\t\tsecond reworded\n+\t\t\tEOF\n+\n+\t\t\t$must_fail git verify-commit HEAD~ &&\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'can reword tip of a branch' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -77,6 +109,37 @@ test_expect_success 'can reword commit in the middle' '\n \t)\n '\n \n+test_reword_gpg_sign ! false\n+test_reword_gpg_sign   true\n+test_reword_gpg_sign   false --gpg-sign\n+test_reword_gpg_sign ! true  --no-gpg-sign\n+test_reword_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_reword_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n+test_expect_success GPG 'reword uses an explicit signing key for rewritten history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\treword_with_message -SB7227189 HEAD~ <<-EOF &&\n+\t\tsecond reworded\n+\t\tEOF\n+\n+\t\tgit verify-commit HEAD~ &&\n+\t\tgit verify-commit HEAD &&\n+\t\tgit log -2 --format=%GK >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\t65A0EEA02E30CAD7\n+\t\t65A0EEA02E30CAD7\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n test_expect_success 'can reword commit in the middle even on detached head' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\ndiff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh\nindex 8ed0cebb50..e96f492cc6 100755\n--- a/t/t3452-history-split.sh\n+++ b/t/t3452-history-split.sh\n@@ -4,6 +4,7 @@ test_description='tests for git-history split subcommand'\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY/lib-log-graph.sh\"\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n # The fake editor takes multiple arguments, each of which represents a commit\n # message. Subsequent invocations of the editor will then yield those messages\n@@ -36,6 +37,42 @@ expect_tree_entries () {\n \ttest_cmp expect actual\n }\n \n+test_split_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"split $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit initial &&\n+\t\t\ttouch bar foo &&\n+\t\t\tgit add . &&\n+\t\t\tgit commit -m split-me &&\n+\t\t\ttest_commit tip &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\tset_fake_editor 'first' 'second' &&\n+\t\t\tgit history split $* HEAD~ <<-EOF &&\n+\t\t\ty\n+\t\t\tn\n+\t\t\tEOF\n+\n+\t\t\t$must_fail git verify-commit HEAD~2 &&\n+\t\t\t$must_fail git verify-commit HEAD~ &&\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'refuses to work with merge commits' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -141,6 +178,13 @@ test_expect_success 'can split up tip commit' '\n \t)\n '\n \n+test_split_gpg_sign ! false\n+test_split_gpg_sign   true\n+test_split_gpg_sign   false --gpg-sign\n+test_split_gpg_sign ! true  --no-gpg-sign\n+test_split_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_split_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n test_expect_success 'can split up root commit' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\ndiff --git a/t/t3453-history-fixup.sh b/t/t3453-history-fixup.sh\nindex 868298e248..cd20a23115 100755\n--- a/t/t3453-history-fixup.sh\n+++ b/t/t3453-history-fixup.sh\n@@ -3,6 +3,7 @@\n test_description='tests for git-history fixup subcommand'\n \n . ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n fixup_with_message () {\n \tcat >message &&\n@@ -21,6 +22,37 @@ expect_changes () {\n \ttest_cmp expect actual\n }\n \n+test_fixup_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"fixup $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit first &&\n+\t\t\ttest_commit second &&\n+\t\t\ttest_commit third &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\techo fix >>second.t &&\n+\t\t\tgit add second.t &&\n+\t\t\tgit history fixup $* HEAD~ &&\n+\n+\t\t\t$must_fail git verify-commit HEAD~ &&\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'errors on missing commit argument' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -229,6 +261,13 @@ test_expect_success 'preserves commit message and authorship' '\n \t)\n '\n \n+test_fixup_gpg_sign ! false\n+test_fixup_gpg_sign   true\n+test_fixup_gpg_sign   false --gpg-sign\n+test_fixup_gpg_sign ! true  --no-gpg-sign\n+test_fixup_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_fixup_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n test_expect_success 'updates all descendant branches by default' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo --initial-branch=main &&\ndiff --git a/t/t3454-history-drop.sh b/t/t3454-history-drop.sh\nindex 68a86d1e37..5b21078a7e 100755\n--- a/t/t3454-history-drop.sh\n+++ b/t/t3454-history-drop.sh\n@@ -4,6 +4,7 @@ test_description='tests for git-history drop subcommand'\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY/lib-log-graph.sh\"\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n expect_graph () {\n \tcat >expect &&\n@@ -16,6 +17,34 @@ expect_log () {\n \ttest_cmp expect actual\n }\n \n+test_drop_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"drop $* with commit.gpgsign=$conf $will sign replayed descendants\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit first &&\n+\t\t\ttest_commit second &&\n+\t\t\ttest_commit third &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\tgit history drop $* HEAD~ &&\n+\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'errors on missing commit argument' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -88,6 +117,27 @@ test_expect_success 'drops a commit in the middle and replays descendants' '\n \t)\n '\n \n+test_drop_gpg_sign ! false\n+test_drop_gpg_sign   true\n+test_drop_gpg_sign   false --gpg-sign\n+test_drop_gpg_sign ! true  --no-gpg-sign\n+test_drop_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_drop_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n+test_expect_success GPG 'drop has no commit to sign when dropping the tip' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\n+\t\tgit history drop --gpg-sign HEAD &&\n+\n+\t\ttest_must_fail git verify-commit HEAD\n+\t)\n+'\n+\n test_expect_success 'drops the HEAD commit' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\ndiff --git a/t/t3455-history-squash.sh b/t/t3455-history-squash.sh\nindex d21e9d9fc4..237afdfa26 100755\n--- a/t/t3455-history-squash.sh\n+++ b/t/t3455-history-squash.sh\n@@ -3,6 +3,7 @@\n test_description='tests for git-history squash subcommand'\n \n . ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n stage_file () {\n \tprintf \"%s\\n\" \"$1\" >file &&\n@@ -40,6 +41,66 @@ check_commit_author () {\n \ttest_cmp expect actual\n }\n \n+test_squash_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"squash $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit first &&\n+\t\t\ttest_commit second &&\n+\t\t\ttest_commit third &&\n+\t\t\ttest_commit fourth &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\tgit history squash --no-edit $* HEAD~3..HEAD~1 &&\n+\n+\t\t\t$must_fail git verify-commit HEAD~ &&\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n+test_squash_gpg_sign ! false\n+test_squash_gpg_sign   true\n+test_squash_gpg_sign   false --gpg-sign\n+test_squash_gpg_sign ! true  --no-gpg-sign\n+test_squash_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_squash_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n+test_expect_success GPG 'squash uses an explicit signing key for rewritten history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\n+\t\tgit history squash --no-edit -SB7227189 HEAD~3..HEAD~1 &&\n+\n+\t\tgit verify-commit HEAD~ &&\n+\t\tgit verify-commit HEAD &&\n+\t\tgit log -2 --format=%GK >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\t65A0EEA02E30CAD7\n+\t\t65A0EEA02E30CAD7\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n test_expect_success 'setup linear history touching two files' '\n \ttest_commit base file a start &&\n \tGIT_AUTHOR_NAME=One GIT_AUTHOR_EMAIL=one@example.com \\\n-- \n2.56.0\n\n"},{"id":"554021","messageId":"xmqq7bjzvhxq.fsf@gitster.g","threadId":"65920","inReplyTo":"20261002132718.3830-1-git@5ouma.me","subject":"Re: [PATCH v4 0/2] history: sign rewritten commits","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-02T22:47:13Z","receivedAt":"2026-10-02T22:47:15Z","isPatch":true,"body":"Souma <git@5ouma.me> writes:\n\n> History rewriting creates commits through two paths: the history commands\n> write replacement commits directly, while the replay machinery recreates\n> descendants above the rewritten range. Neither path currently honors\n> `commit.gpgSign` or an explicit signing request, so rewriting signed history\n> can leave the resulting commits unsigned.\n\nWith this topic merged, 'seen' seems to break t9902.\n\nAlso, is this expected?\n\n    $ git history split --git-completion-helper\n    --update-refs= --dry-run --gpg-sign --no-dry-run -- --no-gpg-sign\n\n"},{"id":"554060","messageId":"2ddd0d1c-82e8-4793-b109-6c37a88bda23@app.fastmail.com","threadId":"65920","inReplyTo":"xmqq7bjzvhxq.fsf@gitster.g","subject":"Re: [PATCH v4 0/2] history: sign rewritten commits","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-10-03T13:38:35Z","receivedAt":"2026-10-03T13:38:56Z","isPatch":true,"body":"Yes, this is expected. The output is an internal completion protocol with options before  --  and generated negated options after it. Other similar commands, such as git rebase and git commit, use the same parse-options behavior and produce the same pattern.\n"},{"id":"554061","messageId":"20261003134058.23494-1-git@5ouma.me","threadId":"65920","inReplyTo":"20260703145037.69832-1-git@5ouma.me","subject":"[PATCH v5 0/2] history: sign rewritten commits","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-10-03T13:40:56Z","receivedAt":"2026-10-03T13:41:27Z","isPatch":true,"body":"History rewriting creates commits through two paths: the history commands\nwrite replacement commits directly, while the replay machinery recreates\ndescendants above the rewritten range. Neither path currently honors\n`commit.gpgSign` or an explicit signing request, so rewriting signed history\ncan leave the resulting commits unsigned.\n\nAdd a signing-key option to the replay API, then have the history commands\npass the selected signer through both paths. Expose the standard\n`-S`/`--gpg-sign[=<key-id>]` and `--no-gpg-sign` options for `drop`, `fixup`,\n`reword`, `split`, and `squash`. This applies one signing policy to every\ncommit created by the rewrite, including both commits from `split`, the\ncommit from `squash`, and replayed descendants.\n\nThe behavior follows rebase, cherry-pick, and revert:\n`commit.gpgSign` supplies the default, command-line options override it, and\nthe last command-line option wins. The signature attests the current\ncommitter's rewrite while preserving the original author identity.\n\nChanges since v4:\n\n - Add Git completion coverage for `--gpg-sign` and `--no-gpg-sign` to the\n   history subcommand option tests\n\nSouma (2):\n  replay: allow callers to sign commits\n  history: sign rewritten commits\n\n Documentation/git-history.adoc | 18 +++++--\n builtin/history.c              | 96 +++++++++++++++++++++++++---------\n replay.c                       | 13 +++--\n replay.h                       |  6 +++\n t/t3451-history-reword.sh      | 63 ++++++++++++++++++++++\n t/t3452-history-split.sh       | 44 ++++++++++++++++\n t/t3453-history-fixup.sh       | 39 ++++++++++++++\n t/t3454-history-drop.sh        | 50 ++++++++++++++++++\n t/t3455-history-squash.sh      | 61 +++++++++++++++++++++\n t/t9902-completion.sh          |  2 +\n 10 files changed, 358 insertions(+), 34 deletions(-)\n\nRange-diff against v4:\n1:  d45cce8e25 = 1:  d45cce8e25 replay: allow callers to sign commits\n2:  8b4766fc0e ! 2:  65f3de1562 history: sign rewritten commits\n    @@ t/t3455-history-squash.sh: check_commit_author () {\n      test_expect_success 'setup linear history touching two files' '\n      \ttest_commit base file a start &&\n      \tGIT_AUTHOR_NAME=One GIT_AUTHOR_EMAIL=one@example.com \\\n    +\n    + ## t/t9902-completion.sh ##\n    +@@ t/t9902-completion.sh: test_expect_success 'git history subcommand options' '\n    + \ttest_completion \"git history split main --\" <<-\\EOF &&\n    + \t--update-refs=Z\n    + \t--dry-run Z\n    ++\t--gpg-sign Z\n    ++\t--no-... Z\n    + \t--no-dry-run Z\n    + \tEOF\n    + \ttest_completion \"git history fixup --upd\" \"--update-refs=\" &&\n-- \n2.56.0\n"},{"id":"554062","messageId":"20261003134058.23494-2-git@5ouma.me","threadId":"65920","inReplyTo":"20260703145037.69832-1-git@5ouma.me","subject":"[PATCH v5 1/2] replay: allow callers to sign commits","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-10-03T13:40:57Z","receivedAt":"2026-10-03T13:41:29Z","isPatch":true,"body":"Add a signing-key option to replay_revisions_options and pass it to\ncommit_tree_extended() when creating replayed commits.\n\nThis provides the replay infrastructure for history commands to sign\nreplayed descendants.\n\nSigned-off-by: Souma <git@5ouma.me>\n---\n replay.c | 13 ++++++++-----\n replay.h |  6 ++++++\n 2 files changed, 14 insertions(+), 5 deletions(-)\n\ndiff --git a/replay.c b/replay.c\nindex f415103023..3e8a70bce1 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -85,13 +85,13 @@ static struct commit *create_commit(struct repository *repo,\n \t\t\t\t    struct tree *tree,\n \t\t\t\t    struct commit *based_on,\n \t\t\t\t    struct commit *parent,\n-\t\t\t\t    enum replay_mode mode)\n+\t\t\t\t    enum replay_mode mode,\n+\t\t\t\t    const char *sign_commit)\n {\n \tstruct object_id ret;\n \tstruct object *obj = NULL;\n \tstruct commit_list *parents = NULL;\n \tchar *author = NULL;\n-\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n \tstruct commit_extra_header *extra = NULL;\n \tstruct strbuf msg = STRBUF_INIT;\n \tconst char *out_enc = get_commit_output_encoding();\n@@ -288,7 +288,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \t\t\t\t\t  struct merge_options *merge_opt,\n \t\t\t\t\t  struct merge_result *result,\n \t\t\t\t\t  enum replay_mode mode,\n-\t\t\t\t\t  enum replay_empty_commit_action empty)\n+\t\t\t\t\t  enum replay_empty_commit_action empty,\n+\t\t\t\t\t  const char *sign_commit)\n {\n \tstruct tree *pickme_tree, *base_tree, *replayed_base_tree;\n \n@@ -361,7 +362,8 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \t\t}\n \t}\n \n-\treturn create_commit(repo, result->tree, pickme, replayed_base, mode);\n+\treturn create_commit(repo, result->tree, pickme, replayed_base, mode,\n+\t\t\t\t\t    sign_commit);\n }\n \n void replay_result_release(struct replay_result *result)\n@@ -481,7 +483,8 @@ int replay_revisions(struct rev_info *revs,\n \n \t\t\tlast_commit = pick_regular_commit(revs->repo, commit, base,\n \t\t\t\t\t\t\t  &merge_opt, &result,\n-\t\t\t\t\t\t\t  mode, opts->empty);\n+\t\t\t\t\t\t\t  mode, opts->empty,\n+\t\t\t\t\t\t\t  opts->sign_commit);\n \t\t}\n \n \t\tif (!last_commit)\ndiff --git a/replay.h b/replay.h\nindex 2c71afbfde..2eb7704b74 100644\n--- a/replay.h\n+++ b/replay.h\n@@ -57,6 +57,12 @@ struct replay_revisions_options {\n \t */\n \tint contained;\n \n+\t/*\n+\t * Key used to sign newly-created commits. An empty string requests the\n+\t * default configured signing key, and NULL disables signing.\n+\t */\n+\tconst char *sign_commit;\n+\n \t/*\n \t * Controls what to do when a replayed commit becomes empty.\n \t * Defaults to REPLAY_EMPTY_COMMIT_DROP.\n-- \n2.56.0\n\n"},{"id":"554063","messageId":"20261003134058.23494-3-git@5ouma.me","threadId":"65920","inReplyTo":"20260703145037.69832-1-git@5ouma.me","subject":"[PATCH v5 2/2] history: sign rewritten commits","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-10-03T13:40:58Z","receivedAt":"2026-10-03T13:41:31Z","isPatch":true,"body":"Add --gpg-sign/--no-gpg-sign support to git history and honor\ncommit.gpgSign when creating replacement commits. Thread the selected\nsigning key through direct rewrites and replayed descendants while\npreserving the original author identity.\n\nLoad history configuration before parsing command-line options so\ncommand-line signing options override commit.gpgSign.\n\nCover configuration, command-line precedence, explicit keys, split\ncommits, and replayed descendants with GPG-gated tests.\n\nSigned-off-by: Souma <git@5ouma.me>\n---\n Documentation/git-history.adoc | 18 +++++--\n builtin/history.c              | 96 +++++++++++++++++++++++++---------\n t/t3451-history-reword.sh      | 63 ++++++++++++++++++++++\n t/t3452-history-split.sh       | 44 ++++++++++++++++\n t/t3453-history-fixup.sh       | 39 ++++++++++++++\n t/t3454-history-drop.sh        | 50 ++++++++++++++++++\n t/t3455-history-squash.sh      | 61 +++++++++++++++++++++\n t/t9902-completion.sh          |  2 +\n 8 files changed, 344 insertions(+), 29 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 2e2e31f521..1fcc30150a 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,11 +8,11 @@ git-history - EXPERIMENTAL: Rewrite history\n SYNOPSIS\n --------\n [synopsis]\n-git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]\n-git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]\n-git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\n-git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\n-git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] <revision-range>\n+git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\n+git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\n+git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]\n+git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]\n+git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] [--[no-]gpg-sign[=<key-id>]] <revision-range>\n \n DESCRIPTION\n -----------\n@@ -180,6 +180,14 @@ OPTIONS\n `--reedit-message`::\n \tOpen an editor to modify the target commit's message.\n \n+`-S[<key-id>]`::\n+`--gpg-sign[=<key-id>]`::\n+`--no-gpg-sign`::\n+\tGPG-sign rewritten commits. The _<key-id>_ argument is optional and\n+\tdefaults to the committer identity; if specified, it must be stuck to\n+\tthe option without a space. `--no-gpg-sign` is useful to countermand\n+\tboth `commit.gpgSign` configuration and earlier `--gpg-sign`.\n+\n `--empty=(drop|keep|abort)`::\n \tControl what happens when a commit becomes empty as a result of the\n \tfixup. This can happen in two situations:\ndiff --git a/builtin/history.c b/builtin/history.c\nindex 54cea3523f..43ba76f5a1 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -30,15 +30,15 @@\n #include \"wt-status.h\"\n \n #define GIT_HISTORY_DROP_USAGE \\\n-\tN_(\"git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]\")\n+\tN_(\"git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\")\n #define GIT_HISTORY_FIXUP_USAGE \\\n-\tN_(\"git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]\")\n+\tN_(\"git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]\")\n #define GIT_HISTORY_REWORD_USAGE \\\n-\tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\")\n+\tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]\")\n #define GIT_HISTORY_SPLIT_USAGE \\\n-\tN_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\")\n+\tN_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]\")\n #define GIT_HISTORY_SQUASH_USAGE \\\n-\tN_(\"git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] <revision-range>\")\n+\tN_(\"git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] [--[no-]gpg-sign[=<key-id>]] <revision-range>\")\n \n static void change_data_free(void *util, const char *str UNUSED)\n {\n@@ -110,6 +110,30 @@ enum commit_tree_flags {\n \tCOMMIT_TREE_EDIT_MESSAGE = (1 << 0),\n };\n \n+static int history_config(const char *var, const char *value,\n+\t\t\t  const struct config_context *ctx, void *data)\n+{\n+\tconst char **sign_commit = data;\n+\n+\tif (!strcmp(var, \"commit.gpgsign\")) {\n+\t\t*sign_commit = git_config_bool(var, value) ? \"\" : NULL;\n+\t\treturn 0;\n+\t}\n+\n+\treturn git_default_config(var, value, ctx, NULL);\n+}\n+\n+#define OPT_HISTORY_GPG_SIGN(v) { \\\n+\t.type = OPTION_STRING, \\\n+\t.short_name = 'S', \\\n+\t.long_name = \"gpg-sign\", \\\n+\t.value = (v), \\\n+\t.argh = N_(\"key-id\"), \\\n+\t.help = N_(\"GPG-sign rewritten commits\"), \\\n+\t.flags = PARSE_OPT_OPTARG, \\\n+\t.defval = (intptr_t)\"\", \\\n+}\n+\n static int commit_tree_ext(struct repository *repo,\n \t\t\t   const char *action,\n \t\t\t   struct commit *commit_with_message,\n@@ -117,6 +141,7 @@ static int commit_tree_ext(struct repository *repo,\n \t\t\t   const struct commit_list *parents,\n \t\t\t   const struct object_id *old_tree,\n \t\t\t   const struct object_id *new_tree,\n+\t\t\t   const char *sign_commit,\n \t\t\t   struct commit **out,\n \t\t\t   enum commit_tree_flags flags)\n {\n@@ -160,7 +185,7 @@ static int commit_tree_ext(struct repository *repo,\n \n \tret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree,\n \t\t\t\t   parents, &rewritten_commit_oid, original_author,\n-\t\t\t\t   NULL, NULL, original_extra_headers);\n+\t\t\t\t   NULL, sign_commit, original_extra_headers);\n \tif (ret < 0)\n \t\tgoto out;\n \n@@ -196,6 +221,7 @@ static int first_parent_tree_oid(struct repository *repo,\n static int commit_tree_with_edited_message(struct repository *repo,\n \t\t\t\t\t   const char *action,\n \t\t\t\t\t   struct commit *original,\n+\t\t\t\t\t   const char *sign_commit,\n \t\t\t\t\t   struct commit **out)\n {\n \tstruct object_id parent_tree_oid;\n@@ -207,7 +233,8 @@ static int commit_tree_with_edited_message(struct repository *repo,\n \t\treturn -1;\n \n \treturn commit_tree_ext(repo, action, original, NULL, original->parents,\n-\t\t\t       &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);\n+\t\t\t       &parent_tree_oid, tree_oid, sign_commit, out,\n+\t\t\t       COMMIT_TREE_EDIT_MESSAGE);\n }\n \n enum ref_action {\n@@ -363,12 +390,14 @@ static int compute_pending_ref_updates(struct rev_info *revs,\n \t\t\t\t       enum ref_action action,\n \t\t\t\t       struct commit *original,\n \t\t\t\t       struct commit *rewritten,\n+\t\t\t\t       const char *sign_commit,\n \t\t\t\t       enum replay_empty_commit_action empty,\n \t\t\t\t       struct replay_result *result)\n {\n \tconst struct name_decoration *decoration;\n \tstruct replay_revisions_options opts = {\n \t\t.empty = empty,\n+\t\t.sign_commit = sign_commit,\n \t};\n \tchar hex[GIT_MAX_HEXSZ + 1];\n \tbool detached_head;\n@@ -473,13 +502,14 @@ static int handle_reference_updates(struct rev_info *revs,\n \t\t\t\t    struct commit *rewritten,\n \t\t\t\t    const char *reflog_msg,\n \t\t\t\t    int dry_run,\n+\t\t\t\t    const char *sign_commit,\n \t\t\t\t    enum replay_empty_commit_action empty)\n {\n \tstruct replay_result result = { 0 };\n \tint ret;\n \n \tret = compute_pending_ref_updates(revs, action, original, rewritten,\n-\t\t\t\t\t  empty, &result);\n+\t\t\t\t\t  sign_commit, empty, &result);\n \tif (ret)\n \t\tgoto out;\n \n@@ -533,6 +563,7 @@ static int cmd_history_fixup(int argc,\n \tenum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;\n \tenum ref_action action = REF_ACTION_DEFAULT;\n \tenum commit_tree_flags flags = 0;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -546,6 +577,7 @@ static int cmd_history_fixup(int argc,\n \t\tOPT_CALLBACK_F(0, \"empty\", &empty, \"(drop|keep|abort)\",\n \t\t\t       N_(\"how to handle commits that become empty\"),\n \t\t\t       PARSE_OPT_NONEG, parse_opt_empty),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct merge_result merge_result = { 0 };\n@@ -557,12 +589,12 @@ static int cmd_history_fixup(int argc,\n \tbool skip_commit = false;\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc != 1) {\n \t\tret = error(_(\"command expects a single revision\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -687,7 +719,7 @@ static int cmd_history_fixup(int argc,\n \tif (!skip_commit) {\n \t\tret = commit_tree_ext(repo, \"fixup\", original, NULL, original->parents,\n \t\t\t\t      &original_tree->object.oid, &merge_result.tree->object.oid,\n-\t\t\t\t      &rewritten, flags);\n+\t\t\t\t      sign_commit, &rewritten, flags);\n \t\tif (ret < 0) {\n \t\t\tret = error(_(\"failed writing fixed-up commit\"));\n \t\t\tgoto out;\n@@ -697,7 +729,7 @@ static int cmd_history_fixup(int argc,\n \tstrbuf_addf(&reflog_msg, \"fixup: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf, dry_run, empty);\n+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit, empty);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\n@@ -722,6 +754,7 @@ static int cmd_history_reword(int argc,\n \t\tNULL,\n \t};\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -729,6 +762,7 @@ static int cmd_history_reword(int argc,\n \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n \t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n \t\t\t N_(\"perform a dry-run without updating any refs\")),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n@@ -736,12 +770,12 @@ static int cmd_history_reword(int argc,\n \tstruct rev_info revs = { 0 };\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc != 1) {\n \t\tret = error(_(\"command expects a single revision\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -756,7 +790,8 @@ static int cmd_history_reword(int argc,\n \tif (ret)\n \t\tgoto out;\n \n-\tret = commit_tree_with_edited_message(repo, \"reworded\", original, &rewritten);\n+\tret = commit_tree_with_edited_message(repo, \"reworded\", original,\n+\t\t\t\t\t      sign_commit, &rewritten);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing reworded commit\"));\n \t\tgoto out;\n@@ -765,7 +800,8 @@ static int cmd_history_reword(int argc,\n \tstrbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);\n+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit,\n+\t\t\t\t       REPLAY_EMPTY_COMMIT_ABORT);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\n@@ -831,6 +867,7 @@ static int write_ondisk_index(struct repository *repo,\n static int split_commit(struct repository *repo,\n \t\t\tstruct commit *original,\n \t\t\tstruct pathspec *pathspec,\n+\t\t\tconst char *sign_commit,\n \t\t\tstruct commit **out)\n {\n \tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n@@ -900,8 +937,10 @@ static int split_commit(struct repository *repo,\n \t * The first commit is constructed from the split-out tree. The base\n \t * that shall be diffed against is the parent of the original commit.\n \t */\n-\tret = commit_tree_ext(repo, \"split-out\", original, NULL, original->parents, &parent_tree_oid,\n-\t\t\t      &split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);\n+\tret = commit_tree_ext(repo, \"split-out\", original, NULL, original->parents,\n+\t\t\t      &parent_tree_oid, &split_tree->object.oid, sign_commit,\n+\t\t\t      &first_commit,\n+\t\t\t      COMMIT_TREE_EDIT_MESSAGE);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing first commit\"));\n \t\tgoto out;\n@@ -918,7 +957,8 @@ static int split_commit(struct repository *repo,\n \tnew_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;\n \n \tret = commit_tree_ext(repo, \"split-out\", original, NULL, parents, old_tree_oid,\n-\t\t\t      new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);\n+\t\t\t      new_tree_oid, sign_commit, &second_commit,\n+\t\t\t      COMMIT_TREE_EDIT_MESSAGE);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing second commit\"));\n \t\tgoto out;\n@@ -946,6 +986,7 @@ static int cmd_history_split(int argc,\n \t\tNULL,\n \t};\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -953,6 +994,7 @@ static int cmd_history_split(int argc,\n \t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n \t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n \t\t\t N_(\"perform a dry-run without updating any refs\")),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct commit *original, *rewritten = NULL;\n@@ -961,12 +1003,12 @@ static int cmd_history_split(int argc,\n \tstruct rev_info revs = { 0 };\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc < 1) {\n \t\tret = error(_(\"command expects a committish\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -992,14 +1034,15 @@ static int cmd_history_split(int argc,\n \t\tgoto out;\n \t}\n \n-\tret = split_commit(repo, original, &pathspec, &rewritten);\n+\tret = split_commit(repo, original, &pathspec, sign_commit, &rewritten);\n \tif (ret < 0)\n \t\tgoto out;\n \n \tstrbuf_addf(&reflog_msg, \"split: updating %s\", argv[0]);\n \n \tret = handle_reference_updates(&revs, action, original, rewritten,\n-\t\t\t\t       reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);\n+\t\t\t\t       reflog_msg.buf, dry_run, sign_commit,\n+\t\t\t\t       REPLAY_EMPTY_COMMIT_ABORT);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\n@@ -1579,6 +1622,7 @@ static int cmd_history_squash(int argc,\n \t\tNULL,\n \t};\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tint edit = 1;\n \tstruct option options[] = {\n@@ -1589,6 +1633,7 @@ static int cmd_history_squash(int argc,\n \t\t\t N_(\"perform a dry-run without updating any refs\")),\n \t\tOPT_BOOL('e', \"edit\", &edit,\n \t\t\t N_(\"edit the commit message\")),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n@@ -1598,13 +1643,13 @@ static int cmd_history_squash(int argc,\n \tstruct rev_info revs = { 0 };\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage,\n \t\t\t     PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_ARGV0);\n \tif (argc < 2) {\n \t\tret = error(_(\"command expects a revision range\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -1629,7 +1674,7 @@ static int cmd_history_squash(int argc,\n \n \tret = commit_tree_ext(repo, \"squash\", oldest, message_template,\n \t\t\t      oldest->parents, base_tree_oid, tip_tree_oid,\n-\t\t\t      &rewritten,\n+\t\t\t      sign_commit, &rewritten,\n \t\t\t      edit ? COMMIT_TREE_EDIT_MESSAGE : 0);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed writing squashed commit\"));\n@@ -1638,6 +1683,7 @@ static int cmd_history_squash(int argc,\n \n \tret = handle_reference_updates(&revs, action, tip, rewritten,\n \t\t\t\t       reflog_msg.buf, dry_run,\n+\t\t\t\t       sign_commit,\n \t\t\t\t       REPLAY_EMPTY_COMMIT_ABORT);\n \tif (ret < 0) {\n \t\tret = error(_(\"failed replaying descendants\"));\n@@ -1728,6 +1774,7 @@ static int cmd_history_drop(int argc,\n \t};\n \tenum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;\n \tenum ref_action action = REF_ACTION_DEFAULT;\n+\tconst char *sign_commit = NULL;\n \tint dry_run = 0;\n \tstruct option options[] = {\n \t\tOPT_CALLBACK_F(0, \"update-refs\", &action, \"(branches|head)\",\n@@ -1738,6 +1785,7 @@ static int cmd_history_drop(int argc,\n \t\tOPT_CALLBACK_F(0, \"empty\", &empty, \"(drop|keep|abort)\",\n \t\t\t       N_(\"how to handle descendants that become empty\"),\n \t\t\t       PARSE_OPT_NONEG, parse_opt_empty),\n+\t\tOPT_HISTORY_GPG_SIGN(&sign_commit),\n \t\tOPT_END(),\n \t};\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n@@ -1748,12 +1796,12 @@ static int cmd_history_drop(int argc,\n \tbool head_moves = false;\n \tint ret;\n \n+\trepo_config(repo, history_config, &sign_commit);\n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \tif (argc != 1) {\n \t\tret = error(_(\"command expects a single revision\"));\n \t\tgoto out;\n \t}\n-\trepo_config(repo, git_default_config, NULL);\n \n \tif (action == REF_ACTION_DEFAULT)\n \t\taction = REF_ACTION_BRANCHES;\n@@ -1781,7 +1829,7 @@ static int cmd_history_drop(int argc,\n \trewritten = original->parents->item;\n \n \tret = compute_pending_ref_updates(&revs, action, original, rewritten,\n-\t\t\t\t\t  empty, &result);\n+\t\t\t\t\t  sign_commit, empty, &result);\n \tif (ret) {\n \t\tret = error(_(\"failed replaying descendants\"));\n \t\tgoto out;\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex de7b357685..6dbe2143d3 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -4,6 +4,7 @@ test_description='tests for git-history reword subcommand'\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY/lib-log-graph.sh\"\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n reword_with_message () {\n \tcat >message &&\n@@ -26,6 +27,37 @@ expect_log () {\n \ttest_cmp expect actual\n }\n \n+test_reword_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"reword $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit first &&\n+\t\t\ttest_commit second &&\n+\t\t\ttest_commit third &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\treword_with_message $* HEAD~ <<-EOF &&\n+\t\t\tsecond reworded\n+\t\t\tEOF\n+\n+\t\t\t$must_fail git verify-commit HEAD~ &&\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'can reword tip of a branch' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -77,6 +109,37 @@ test_expect_success 'can reword commit in the middle' '\n \t)\n '\n \n+test_reword_gpg_sign ! false\n+test_reword_gpg_sign   true\n+test_reword_gpg_sign   false --gpg-sign\n+test_reword_gpg_sign ! true  --no-gpg-sign\n+test_reword_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_reword_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n+test_expect_success GPG 'reword uses an explicit signing key for rewritten history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\treword_with_message -SB7227189 HEAD~ <<-EOF &&\n+\t\tsecond reworded\n+\t\tEOF\n+\n+\t\tgit verify-commit HEAD~ &&\n+\t\tgit verify-commit HEAD &&\n+\t\tgit log -2 --format=%GK >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\t65A0EEA02E30CAD7\n+\t\t65A0EEA02E30CAD7\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n test_expect_success 'can reword commit in the middle even on detached head' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\ndiff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh\nindex 8ed0cebb50..e96f492cc6 100755\n--- a/t/t3452-history-split.sh\n+++ b/t/t3452-history-split.sh\n@@ -4,6 +4,7 @@ test_description='tests for git-history split subcommand'\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY/lib-log-graph.sh\"\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n # The fake editor takes multiple arguments, each of which represents a commit\n # message. Subsequent invocations of the editor will then yield those messages\n@@ -36,6 +37,42 @@ expect_tree_entries () {\n \ttest_cmp expect actual\n }\n \n+test_split_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"split $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit initial &&\n+\t\t\ttouch bar foo &&\n+\t\t\tgit add . &&\n+\t\t\tgit commit -m split-me &&\n+\t\t\ttest_commit tip &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\tset_fake_editor 'first' 'second' &&\n+\t\t\tgit history split $* HEAD~ <<-EOF &&\n+\t\t\ty\n+\t\t\tn\n+\t\t\tEOF\n+\n+\t\t\t$must_fail git verify-commit HEAD~2 &&\n+\t\t\t$must_fail git verify-commit HEAD~ &&\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'refuses to work with merge commits' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -141,6 +178,13 @@ test_expect_success 'can split up tip commit' '\n \t)\n '\n \n+test_split_gpg_sign ! false\n+test_split_gpg_sign   true\n+test_split_gpg_sign   false --gpg-sign\n+test_split_gpg_sign ! true  --no-gpg-sign\n+test_split_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_split_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n test_expect_success 'can split up root commit' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\ndiff --git a/t/t3453-history-fixup.sh b/t/t3453-history-fixup.sh\nindex 868298e248..cd20a23115 100755\n--- a/t/t3453-history-fixup.sh\n+++ b/t/t3453-history-fixup.sh\n@@ -3,6 +3,7 @@\n test_description='tests for git-history fixup subcommand'\n \n . ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n fixup_with_message () {\n \tcat >message &&\n@@ -21,6 +22,37 @@ expect_changes () {\n \ttest_cmp expect actual\n }\n \n+test_fixup_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"fixup $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit first &&\n+\t\t\ttest_commit second &&\n+\t\t\ttest_commit third &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\techo fix >>second.t &&\n+\t\t\tgit add second.t &&\n+\t\t\tgit history fixup $* HEAD~ &&\n+\n+\t\t\t$must_fail git verify-commit HEAD~ &&\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'errors on missing commit argument' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -229,6 +261,13 @@ test_expect_success 'preserves commit message and authorship' '\n \t)\n '\n \n+test_fixup_gpg_sign ! false\n+test_fixup_gpg_sign   true\n+test_fixup_gpg_sign   false --gpg-sign\n+test_fixup_gpg_sign ! true  --no-gpg-sign\n+test_fixup_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_fixup_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n test_expect_success 'updates all descendant branches by default' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo --initial-branch=main &&\ndiff --git a/t/t3454-history-drop.sh b/t/t3454-history-drop.sh\nindex 68a86d1e37..5b21078a7e 100755\n--- a/t/t3454-history-drop.sh\n+++ b/t/t3454-history-drop.sh\n@@ -4,6 +4,7 @@ test_description='tests for git-history drop subcommand'\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY/lib-log-graph.sh\"\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n expect_graph () {\n \tcat >expect &&\n@@ -16,6 +17,34 @@ expect_log () {\n \ttest_cmp expect actual\n }\n \n+test_drop_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"drop $* with commit.gpgsign=$conf $will sign replayed descendants\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit first &&\n+\t\t\ttest_commit second &&\n+\t\t\ttest_commit third &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\tgit history drop $* HEAD~ &&\n+\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n test_expect_success 'errors on missing commit argument' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n@@ -88,6 +117,27 @@ test_expect_success 'drops a commit in the middle and replays descendants' '\n \t)\n '\n \n+test_drop_gpg_sign ! false\n+test_drop_gpg_sign   true\n+test_drop_gpg_sign   false --gpg-sign\n+test_drop_gpg_sign ! true  --no-gpg-sign\n+test_drop_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_drop_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n+test_expect_success GPG 'drop has no commit to sign when dropping the tip' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\n+\t\tgit history drop --gpg-sign HEAD &&\n+\n+\t\ttest_must_fail git verify-commit HEAD\n+\t)\n+'\n+\n test_expect_success 'drops the HEAD commit' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\ndiff --git a/t/t3455-history-squash.sh b/t/t3455-history-squash.sh\nindex d21e9d9fc4..237afdfa26 100755\n--- a/t/t3455-history-squash.sh\n+++ b/t/t3455-history-squash.sh\n@@ -3,6 +3,7 @@\n test_description='tests for git-history squash subcommand'\n \n . ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-gpg.sh\"\n \n stage_file () {\n \tprintf \"%s\\n\" \"$1\" >file &&\n@@ -40,6 +41,66 @@ check_commit_author () {\n \ttest_cmp expect actual\n }\n \n+test_squash_gpg_sign () {\n+\tmust_fail= will=will\n+\tif test \"x$1\" = \"x!\"\n+\tthen\n+\t\tmust_fail=test_must_fail\n+\t\twill=\"will not\"\n+\t\tshift\n+\tfi\n+\tconf=$1\n+\tshift\n+\n+\ttest_expect_success GPG \"squash $* with commit.gpgsign=$conf $will sign rewritten history\" \"\n+\t\ttest_when_finished 'rm -rf repo' &&\n+\t\tgit init repo &&\n+\t\t(\n+\t\t\tcd repo &&\n+\t\t\ttest_commit first &&\n+\t\t\ttest_commit second &&\n+\t\t\ttest_commit third &&\n+\t\t\ttest_commit fourth &&\n+\n+\t\t\tgit config commit.gpgsign $conf &&\n+\t\t\tgit history squash --no-edit $* HEAD~3..HEAD~1 &&\n+\n+\t\t\t$must_fail git verify-commit HEAD~ &&\n+\t\t\t$must_fail git verify-commit HEAD\n+\t\t)\n+\t\"\n+}\n+\n+test_squash_gpg_sign ! false\n+test_squash_gpg_sign   true\n+test_squash_gpg_sign   false --gpg-sign\n+test_squash_gpg_sign ! true  --no-gpg-sign\n+test_squash_gpg_sign ! true  --gpg-sign --no-gpg-sign\n+test_squash_gpg_sign   false --no-gpg-sign --gpg-sign\n+\n+test_expect_success GPG 'squash uses an explicit signing key for rewritten history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\n+\t\tgit history squash --no-edit -SB7227189 HEAD~3..HEAD~1 &&\n+\n+\t\tgit verify-commit HEAD~ &&\n+\t\tgit verify-commit HEAD &&\n+\t\tgit log -2 --format=%GK >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\t65A0EEA02E30CAD7\n+\t\t65A0EEA02E30CAD7\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n test_expect_success 'setup linear history touching two files' '\n \ttest_commit base file a start &&\n \tGIT_AUTHOR_NAME=One GIT_AUTHOR_EMAIL=one@example.com \\\ndiff --git a/t/t9902-completion.sh b/t/t9902-completion.sh\nindex 978c42c629..f6de74054a 100755\n--- a/t/t9902-completion.sh\n+++ b/t/t9902-completion.sh\n@@ -3236,6 +3236,8 @@ test_expect_success 'git history subcommand options' '\n \ttest_completion \"git history split main --\" <<-\\EOF &&\n \t--update-refs=Z\n \t--dry-run Z\n+\t--gpg-sign Z\n+\t--no-... Z\n \t--no-dry-run Z\n \tEOF\n \ttest_completion \"git history fixup --upd\" \"--update-refs=\" &&\n-- \n2.56.0\n\n"},{"id":"554113","messageId":"xmqq5wzhv9c8.fsf@gitster.g","threadId":"65920","inReplyTo":"20261003134058.23494-1-git@5ouma.me","subject":"Re: [PATCH v5 0/2] history: sign rewritten commits","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-04T14:17:27Z","receivedAt":"2026-10-04T14:17:30Z","isPatch":true,"body":"Souma <git@5ouma.me> writes:\n\n>     +@@ t/t9902-completion.sh: test_expect_success 'git history subcommand options' '\n>     + \ttest_completion \"git history split main --\" <<-\\EOF &&\n>     + \t--update-refs=Z\n>     + \t--dry-run Z\n>     ++\t--gpg-sign Z\n>     ++\t--no-... Z\n>     + \t--no-dry-run Z\n>     + \tEOF\n>     + \ttest_completion \"git history fixup --upd\" \"--update-refs=\" &&\n\nSorry if this is a stupid question but what does \"--no-...\" mean\nhere?  Do we give that as one of the completion candidates,\nliterally with three periods?\n"},{"id":"554149","messageId":"f86f6cfc-56b4-4358-a9b5-95630c6504ed@app.fastmail.com","threadId":"65920","inReplyTo":"xmqq5wzhv9c8.fsf@gitster.g","subject":"Re: [PATCH v5 0/2] history: sign rewritten commits","fromName":"Souma","fromEmail":"git@5ouma.me","sentAt":"2026-10-05T06:59:20Z","receivedAt":"2026-10-05T06:59:42Z","isPatch":true,"body":"It’s a deliberate literal placeholder, not a real option. It indicates that more  --no-*  options are available; typing  --no- expands them like --no-dry-run.\n"}]}