* [PATCH 0/3] history: sign rewritten commits
@ 2026-07-03 14:50 Souma
2026-07-03 14:50 ` [PATCH 1/3] builtin/history: " Souma
` (5 more replies)
0 siblings, 6 replies; 9+ messages in thread
From: Souma @ 2026-07-03 14:50 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Souma
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.
It adds regression tests that cover configuration-driven signing, command-line overrides, and the handling of replayed descendant commits.
Finally, it updates the history documentation to describe the new signing behavior and available options.
Souma (3):
builtin/history: sign rewritten commits
doc: document history signing options
t345x: cover signed history rewrites
Documentation/git-history.adoc | 14 ++++--
builtin/history.c | 80 ++++++++++++++++++++++++++--------
replay.c | 13 +++---
replay.h | 6 +++
t/t3451-history-reword.sh | 39 +++++++++++++++++
t/t3452-history-split.sh | 44 +++++++++++++++++++
t/t3453-history-fixup.sh | 39 +++++++++++++++++
7 files changed, 209 insertions(+), 26 deletions(-)
--
2.55.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH 1/3] builtin/history: sign rewritten commits
2026-07-03 14:50 [PATCH 0/3] history: sign rewritten commits Souma
@ 2026-07-03 14:50 ` Souma
2026-07-16 10:18 ` Patrick Steinhardt
2026-07-03 14:50 ` [PATCH 2/3] doc: document history signing options Souma
` (4 subsequent siblings)
5 siblings, 1 reply; 9+ messages in thread
From: Souma @ 2026-07-03 14:50 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Souma
The history commands create replacement commits directly instead of
using the sequencer or the commit porcelain. As a result, rewritten
commits ignore commit.gpgsign and cannot be signed on demand.
Read the usual signing configuration before parsing history options.
Add the commit-style -S/--gpg-sign knob, and pass the selected
signing key through direct rewrites and replayed descendants.
Signed-off-by: Souma <git@5ouma.me>
---
builtin/history.c | 80 ++++++++++++++++++++++++++++++++++++-----------
replay.c | 13 +++++---
replay.h | 6 ++++
3 files changed, 76 insertions(+), 23 deletions(-)
diff --git a/builtin/history.c b/builtin/history.c
index 091465a59e..8d669cf539 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -25,11 +25,11 @@
#include "wt-status.h"
#define GIT_HISTORY_FIXUP_USAGE \
- N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]")
+ N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]")
#define GIT_HISTORY_REWORD_USAGE \
- N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]")
+ N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]")
#define GIT_HISTORY_SPLIT_USAGE \
- N_("git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]")
+ N_("git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]")
static void change_data_free(void *util, const char *str UNUSED)
{
@@ -98,6 +98,30 @@ enum commit_tree_flags {
COMMIT_TREE_EDIT_MESSAGE = (1 << 0),
};
+static int history_config(const char *var, const char *value,
+ const struct config_context *ctx, void *data)
+{
+ const char **sign_commit = data;
+
+ if (!strcmp(var, "commit.gpgsign")) {
+ *sign_commit = git_config_bool(var, value) ? "" : NULL;
+ return 0;
+ }
+
+ return git_default_config(var, value, ctx, data);
+}
+
+#define OPT_HISTORY_GPG_SIGN(v) { \
+ .type = OPTION_STRING, \
+ .short_name = 'S', \
+ .long_name = "gpg-sign", \
+ .value = (v), \
+ .argh = N_("key-id"), \
+ .help = N_("GPG-sign rewritten commits"), \
+ .flags = PARSE_OPT_OPTARG, \
+ .defval = (intptr_t) "", \
+}
+
static int commit_tree_ext(struct repository *repo,
const char *action,
struct commit *commit_with_message,
@@ -105,6 +129,7 @@ static int commit_tree_ext(struct repository *repo,
const struct object_id *old_tree,
const struct object_id *new_tree,
struct commit **out,
+ const char *sign_commit,
enum commit_tree_flags flags)
{
const char *exclude_gpgsig[] = {
@@ -144,7 +169,7 @@ static int commit_tree_ext(struct repository *repo,
ret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree,
parents, &rewritten_commit_oid, original_author,
- NULL, NULL, original_extra_headers);
+ NULL, sign_commit, original_extra_headers);
if (ret < 0)
goto out;
@@ -160,7 +185,8 @@ static int commit_tree_ext(struct repository *repo,
static int commit_tree_with_edited_message(struct repository *repo,
const char *action,
struct commit *original,
- struct commit **out)
+ struct commit **out,
+ const char *sign_commit)
{
struct object_id parent_tree_oid;
const struct object_id *tree_oid;
@@ -181,7 +207,8 @@ static int commit_tree_with_edited_message(struct repository *repo,
}
return commit_tree_ext(repo, action, original, original->parents,
- &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);
+ &parent_tree_oid, tree_oid, out, sign_commit,
+ COMMIT_TREE_EDIT_MESSAGE);
}
enum ref_action {
@@ -339,11 +366,13 @@ static int handle_reference_updates(struct rev_info *revs,
struct commit *rewritten,
const char *reflog_msg,
int dry_run,
+ const char *sign_commit,
enum replay_empty_commit_action empty)
{
const struct name_decoration *decoration;
struct replay_revisions_options opts = {
.empty = empty,
+ .sign_commit = sign_commit,
};
struct replay_result result = { 0 };
struct ref_transaction *transaction = NULL;
@@ -491,6 +520,7 @@ static int cmd_history_fixup(int argc,
enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;
enum ref_action action = REF_ACTION_DEFAULT;
enum commit_tree_flags flags = 0;
+ const char *sign_commit = NULL;
int dry_run = 0;
struct option options[] = {
OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
@@ -504,6 +534,7 @@ static int cmd_history_fixup(int argc,
OPT_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)",
N_("how to handle commits that become empty"),
PARSE_OPT_NONEG, parse_opt_empty),
+ OPT_HISTORY_GPG_SIGN(&sign_commit),
OPT_END(),
};
struct merge_result merge_result = { 0 };
@@ -515,12 +546,13 @@ static int cmd_history_fixup(int argc,
bool skip_commit = false;
int ret;
+ repo_config(repo, history_config, &sign_commit);
+
argc = parse_options(argc, argv, prefix, options, usage, 0);
if (argc != 1) {
ret = error(_("command expects a single revision"));
goto out;
}
- repo_config(repo, git_default_config, NULL);
if (action == REF_ACTION_DEFAULT)
action = REF_ACTION_BRANCHES;
@@ -645,7 +677,7 @@ static int cmd_history_fixup(int argc,
if (!skip_commit) {
ret = commit_tree_ext(repo, "fixup", original, original->parents,
&original_tree->object.oid, &merge_result.tree->object.oid,
- &rewritten, flags);
+ &rewritten, sign_commit, flags);
if (ret < 0) {
ret = error(_("failed writing fixed-up commit"));
goto out;
@@ -655,7 +687,7 @@ static int cmd_history_fixup(int argc,
strbuf_addf(&reflog_msg, "fixup: updating %s", argv[0]);
ret = handle_reference_updates(&revs, action, original, rewritten,
- reflog_msg.buf, dry_run, empty);
+ reflog_msg.buf, dry_run, sign_commit, empty);
if (ret < 0) {
ret = error(_("failed replaying descendants"));
goto out;
@@ -680,6 +712,7 @@ static int cmd_history_reword(int argc,
NULL,
};
enum ref_action action = REF_ACTION_DEFAULT;
+ const char *sign_commit = NULL;
int dry_run = 0;
struct option options[] = {
OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
@@ -687,6 +720,7 @@ static int cmd_history_reword(int argc,
PARSE_OPT_NONEG, parse_ref_action),
OPT_BOOL('n', "dry-run", &dry_run,
N_("perform a dry-run without updating any refs")),
+ OPT_HISTORY_GPG_SIGN(&sign_commit),
OPT_END(),
};
struct strbuf reflog_msg = STRBUF_INIT;
@@ -694,12 +728,13 @@ static int cmd_history_reword(int argc,
struct rev_info revs = { 0 };
int ret;
+ repo_config(repo, history_config, &sign_commit);
+
argc = parse_options(argc, argv, prefix, options, usage, 0);
if (argc != 1) {
ret = error(_("command expects a single revision"));
goto out;
}
- repo_config(repo, git_default_config, NULL);
if (action == REF_ACTION_DEFAULT)
action = REF_ACTION_BRANCHES;
@@ -714,7 +749,8 @@ static int cmd_history_reword(int argc,
if (ret)
goto out;
- ret = commit_tree_with_edited_message(repo, "reworded", original, &rewritten);
+ ret = commit_tree_with_edited_message(repo, "reworded", original,
+ &rewritten, sign_commit);
if (ret < 0) {
ret = error(_("failed writing reworded commit"));
goto out;
@@ -723,7 +759,8 @@ static int cmd_history_reword(int argc,
strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
ret = handle_reference_updates(&revs, action, original, rewritten,
- reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
+ reflog_msg.buf, dry_run, sign_commit,
+ REPLAY_EMPTY_COMMIT_ABORT);
if (ret < 0) {
ret = error(_("failed replaying descendants"));
goto out;
@@ -785,7 +822,8 @@ static int write_ondisk_index(struct repository *repo,
static int split_commit(struct repository *repo,
struct commit *original,
struct pathspec *pathspec,
- struct commit **out)
+ struct commit **out,
+ const char *sign_commit)
{
struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct strbuf index_file = STRBUF_INIT;
@@ -862,7 +900,8 @@ static int split_commit(struct repository *repo,
* that shall be diffed against is the parent of the original commit.
*/
ret = commit_tree_ext(repo, "split-out", original, original->parents, &parent_tree_oid,
- &split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);
+ &split_tree->object.oid, &first_commit, sign_commit,
+ COMMIT_TREE_EDIT_MESSAGE);
if (ret < 0) {
ret = error(_("failed writing first commit"));
goto out;
@@ -879,7 +918,8 @@ static int split_commit(struct repository *repo,
new_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;
ret = commit_tree_ext(repo, "split-out", original, parents, old_tree_oid,
- new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);
+ new_tree_oid, &second_commit, sign_commit,
+ COMMIT_TREE_EDIT_MESSAGE);
if (ret < 0) {
ret = error(_("failed writing second commit"));
goto out;
@@ -907,6 +947,7 @@ static int cmd_history_split(int argc,
NULL,
};
enum ref_action action = REF_ACTION_DEFAULT;
+ const char *sign_commit = NULL;
int dry_run = 0;
struct option options[] = {
OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
@@ -914,6 +955,7 @@ static int cmd_history_split(int argc,
PARSE_OPT_NONEG, parse_ref_action),
OPT_BOOL('n', "dry-run", &dry_run,
N_("perform a dry-run without updating any refs")),
+ OPT_HISTORY_GPG_SIGN(&sign_commit),
OPT_END(),
};
struct commit *original, *rewritten = NULL;
@@ -922,12 +964,13 @@ static int cmd_history_split(int argc,
struct rev_info revs = { 0 };
int ret;
+ repo_config(repo, history_config, &sign_commit);
+
argc = parse_options(argc, argv, prefix, options, usage, 0);
if (argc < 1) {
ret = error(_("command expects a committish"));
goto out;
}
- repo_config(repo, git_default_config, NULL);
if (action == REF_ACTION_DEFAULT)
action = REF_ACTION_BRANCHES;
@@ -953,14 +996,15 @@ static int cmd_history_split(int argc,
goto out;
}
- ret = split_commit(repo, original, &pathspec, &rewritten);
+ ret = split_commit(repo, original, &pathspec, &rewritten, sign_commit);
if (ret < 0)
goto out;
strbuf_addf(&reflog_msg, "split: updating %s", argv[0]);
ret = handle_reference_updates(&revs, action, original, rewritten,
- reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
+ reflog_msg.buf, dry_run, sign_commit,
+ REPLAY_EMPTY_COMMIT_ABORT);
if (ret < 0) {
ret = error(_("failed replaying descendants"));
goto out;
diff --git a/replay.c b/replay.c
index da531d5bc6..683c384ef8 100644
--- a/replay.c
+++ b/replay.c
@@ -81,13 +81,13 @@ static struct commit *create_commit(struct repository *repo,
struct tree *tree,
struct commit *based_on,
struct commit *parent,
- enum replay_mode mode)
+ enum replay_mode mode,
+ const char *sign_commit)
{
struct object_id ret;
struct object *obj = NULL;
struct commit_list *parents = NULL;
char *author = NULL;
- char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
struct commit_extra_header *extra = NULL;
struct strbuf msg = STRBUF_INIT;
const char *out_enc = get_commit_output_encoding();
@@ -270,7 +270,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
struct merge_options *merge_opt,
struct merge_result *result,
enum replay_mode mode,
- enum replay_empty_commit_action empty)
+ enum replay_empty_commit_action empty,
+ const char *sign_commit)
{
struct commit *base, *replayed_base;
struct tree *pickme_tree, *base_tree, *replayed_base_tree;
@@ -341,7 +342,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
}
}
- return create_commit(repo, result->tree, pickme, replayed_base, mode);
+ return create_commit(repo, result->tree, pickme, replayed_base, mode,
+ sign_commit);
}
void replay_result_release(struct replay_result *result)
@@ -431,7 +433,8 @@ int replay_revisions(struct rev_info *revs,
last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
mode == REPLAY_MODE_REVERT ? last_commit : onto,
- &merge_opt, &result, mode, opts->empty);
+ &merge_opt, &result, mode, opts->empty,
+ opts->sign_commit);
if (!last_commit)
break;
diff --git a/replay.h b/replay.h
index faf95c7459..c715504d78 100644
--- a/replay.h
+++ b/replay.h
@@ -57,6 +57,12 @@ struct replay_revisions_options {
*/
int contained;
+ /*
+ * Key used to sign newly-created commits. An empty string requests the
+ * default configured signing key, and NULL disables signing.
+ */
+ const char *sign_commit;
+
/*
* Controls what to do when a replayed commit becomes empty.
* Defaults to REPLAY_EMPTY_COMMIT_DROP.
--
2.55.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH 2/3] doc: document history signing options
2026-07-03 14:50 [PATCH 0/3] history: sign rewritten commits Souma
2026-07-03 14:50 ` [PATCH 1/3] builtin/history: " Souma
@ 2026-07-03 14:50 ` Souma
2026-07-16 10:18 ` Patrick Steinhardt
2026-07-03 14:50 ` [PATCH 3/3] t345x: cover signed history rewrites Souma
` (3 subsequent siblings)
5 siblings, 1 reply; 9+ messages in thread
From: Souma @ 2026-07-03 14:50 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Souma
The history manual and usage text should describe the signing controls now
accepted by fixup, reword, and split.
Document -S/--gpg-sign and --no-gpg-sign with the same key-id spelling and
configuration override behavior used by commit-style signing options.
Signed-off-by: Souma <git@5ouma.me>
---
Documentation/git-history.adoc | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 2ba8121795..a1dd5c8580 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -8,9 +8,9 @@ git-history - EXPERIMENTAL: Rewrite history
SYNOPSIS
--------
[synopsis]
-git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]
-git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
-git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]
+git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]
+git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]
+git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]
DESCRIPTION
-----------
@@ -109,6 +109,14 @@ OPTIONS
`--reedit-message`::
Open an editor to modify the target commit's message.
+`-S[<key-id>]`::
+`--gpg-sign[=<key-id>]`::
+`--no-gpg-sign`::
+ GPG-sign rewritten commits. The _<key-id>_ argument is optional and
+ defaults to the committer identity; if specified, it must be stuck to
+ the option without a space. `--no-gpg-sign` is useful to countermand
+ both `commit.gpgSign` configuration and earlier `--gpg-sign`.
+
`--empty=(drop|keep|abort)`::
Control what happens when a commit becomes empty as a result of the
fixup. This can happen in two situations:
--
2.55.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH 3/3] t345x: cover signed history rewrites
2026-07-03 14:50 [PATCH 0/3] history: sign rewritten commits Souma
2026-07-03 14:50 ` [PATCH 1/3] builtin/history: " Souma
2026-07-03 14:50 ` [PATCH 2/3] doc: document history signing options Souma
@ 2026-07-03 14:50 ` Souma
2026-07-17 14:51 ` [PATCH v2 0/2] history: support signing rewritten commits Souma
` (2 subsequent siblings)
5 siblings, 0 replies; 9+ messages in thread
From: Souma @ 2026-07-03 14:50 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Souma
History signing needs regression coverage because these commands bypass the
usual commit machinery and create replacement commits through lower-level
APIs.
Add GPG-gated tests for config-driven signing, command-line signing,
--no-gpg-sign precedence, and signing of replayed descendants after fixup,
reword, and split.
Signed-off-by: Souma <git@5ouma.me>
---
t/t3451-history-reword.sh | 39 ++++++++++++++++++++++++++++++++++
t/t3452-history-split.sh | 44 +++++++++++++++++++++++++++++++++++++++
t/t3453-history-fixup.sh | 39 ++++++++++++++++++++++++++++++++++
3 files changed, 122 insertions(+)
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
index de7b357685..5b41fb6489 100755
--- a/t/t3451-history-reword.sh
+++ b/t/t3451-history-reword.sh
@@ -4,6 +4,7 @@ test_description='tests for git-history reword subcommand'
. ./test-lib.sh
. "$TEST_DIRECTORY/lib-log-graph.sh"
+. "$TEST_DIRECTORY/lib-gpg.sh"
reword_with_message () {
cat >message &&
@@ -26,6 +27,37 @@ expect_log () {
test_cmp expect actual
}
+test_reword_gpg_sign () {
+ must_fail= will=will
+ if test "x$1" = "x!"
+ then
+ must_fail=test_must_fail
+ will="will not"
+ shift
+ fi
+ conf=$1
+ shift
+
+ test_expect_success GPG "reword $* with commit.gpgsign=$conf $will sign rewritten history" "
+ test_when_finished 'rm -rf repo' &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git config commit.gpgsign $conf &&
+ reword_with_message $* HEAD~ <<-EOF &&
+ second reworded
+ EOF
+
+ $must_fail git verify-commit HEAD~ &&
+ $must_fail git verify-commit HEAD
+ )
+ "
+}
+
test_expect_success 'can reword tip of a branch' '
test_when_finished "rm -rf repo" &&
git init repo &&
@@ -77,6 +109,13 @@ test_expect_success 'can reword commit in the middle' '
)
'
+test_reword_gpg_sign ! false
+test_reword_gpg_sign true
+test_reword_gpg_sign false --gpg-sign
+test_reword_gpg_sign ! true --no-gpg-sign
+test_reword_gpg_sign ! true --gpg-sign --no-gpg-sign
+test_reword_gpg_sign false --no-gpg-sign --gpg-sign
+
test_expect_success 'can reword commit in the middle even on detached head' '
test_when_finished "rm -rf repo" &&
git init repo &&
diff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh
index 8ed0cebb50..e96f492cc6 100755
--- a/t/t3452-history-split.sh
+++ b/t/t3452-history-split.sh
@@ -4,6 +4,7 @@ test_description='tests for git-history split subcommand'
. ./test-lib.sh
. "$TEST_DIRECTORY/lib-log-graph.sh"
+. "$TEST_DIRECTORY/lib-gpg.sh"
# The fake editor takes multiple arguments, each of which represents a commit
# message. Subsequent invocations of the editor will then yield those messages
@@ -36,6 +37,42 @@ expect_tree_entries () {
test_cmp expect actual
}
+test_split_gpg_sign () {
+ must_fail= will=will
+ if test "x$1" = "x!"
+ then
+ must_fail=test_must_fail
+ will="will not"
+ shift
+ fi
+ conf=$1
+ shift
+
+ test_expect_success GPG "split $* with commit.gpgsign=$conf $will sign rewritten history" "
+ test_when_finished 'rm -rf repo' &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+ test_commit tip &&
+
+ git config commit.gpgsign $conf &&
+ set_fake_editor 'first' 'second' &&
+ git history split $* HEAD~ <<-EOF &&
+ y
+ n
+ EOF
+
+ $must_fail git verify-commit HEAD~2 &&
+ $must_fail git verify-commit HEAD~ &&
+ $must_fail git verify-commit HEAD
+ )
+ "
+}
+
test_expect_success 'refuses to work with merge commits' '
test_when_finished "rm -rf repo" &&
git init repo &&
@@ -141,6 +178,13 @@ test_expect_success 'can split up tip commit' '
)
'
+test_split_gpg_sign ! false
+test_split_gpg_sign true
+test_split_gpg_sign false --gpg-sign
+test_split_gpg_sign ! true --no-gpg-sign
+test_split_gpg_sign ! true --gpg-sign --no-gpg-sign
+test_split_gpg_sign false --no-gpg-sign --gpg-sign
+
test_expect_success 'can split up root commit' '
test_when_finished "rm -rf repo" &&
git init repo &&
diff --git a/t/t3453-history-fixup.sh b/t/t3453-history-fixup.sh
index 868298e248..cd20a23115 100755
--- a/t/t3453-history-fixup.sh
+++ b/t/t3453-history-fixup.sh
@@ -3,6 +3,7 @@
test_description='tests for git-history fixup subcommand'
. ./test-lib.sh
+. "$TEST_DIRECTORY/lib-gpg.sh"
fixup_with_message () {
cat >message &&
@@ -21,6 +22,37 @@ expect_changes () {
test_cmp expect actual
}
+test_fixup_gpg_sign () {
+ must_fail= will=will
+ if test "x$1" = "x!"
+ then
+ must_fail=test_must_fail
+ will="will not"
+ shift
+ fi
+ conf=$1
+ shift
+
+ test_expect_success GPG "fixup $* with commit.gpgsign=$conf $will sign rewritten history" "
+ test_when_finished 'rm -rf repo' &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git config commit.gpgsign $conf &&
+ echo fix >>second.t &&
+ git add second.t &&
+ git history fixup $* HEAD~ &&
+
+ $must_fail git verify-commit HEAD~ &&
+ $must_fail git verify-commit HEAD
+ )
+ "
+}
+
test_expect_success 'errors on missing commit argument' '
test_when_finished "rm -rf repo" &&
git init repo &&
@@ -229,6 +261,13 @@ test_expect_success 'preserves commit message and authorship' '
)
'
+test_fixup_gpg_sign ! false
+test_fixup_gpg_sign true
+test_fixup_gpg_sign false --gpg-sign
+test_fixup_gpg_sign ! true --no-gpg-sign
+test_fixup_gpg_sign ! true --gpg-sign --no-gpg-sign
+test_fixup_gpg_sign false --no-gpg-sign --gpg-sign
+
test_expect_success 'updates all descendant branches by default' '
test_when_finished "rm -rf repo" &&
git init repo --initial-branch=main &&
--
2.55.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH 2/3] doc: document history signing options
2026-07-03 14:50 ` [PATCH 2/3] doc: document history signing options Souma
@ 2026-07-16 10:18 ` Patrick Steinhardt
0 siblings, 0 replies; 9+ messages in thread
From: Patrick Steinhardt @ 2026-07-16 10:18 UTC (permalink / raw)
To: Souma; +Cc: git, gitster
On Fri, Jul 03, 2026 at 11:50:36PM +0900, Souma wrote:
> The history manual and usage text should describe the signing controls now
> accepted by fixup, reword, and split.
>
> Document -S/--gpg-sign and --no-gpg-sign with the same key-id spelling and
> configuration override behavior used by commit-style signing options.
>
> Signed-off-by: Souma <git@5ouma.me>
> ---
> Documentation/git-history.adoc | 14 +++++++++++---
> 1 file changed, 11 insertions(+), 3 deletions(-)
I think this and the next commit can easily be merged into the first
one. They really belong together, and even worse t0450 probably breaks
with the first commit, only, as the change to the synopsis in our docs
and in the command itself is split up across two commits.
Patrick
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH 1/3] builtin/history: sign rewritten commits
2026-07-03 14:50 ` [PATCH 1/3] builtin/history: " Souma
@ 2026-07-16 10:18 ` Patrick Steinhardt
0 siblings, 0 replies; 9+ messages in thread
From: Patrick Steinhardt @ 2026-07-16 10:18 UTC (permalink / raw)
To: Souma; +Cc: git, gitster
On Fri, Jul 03, 2026 at 11:50:35PM +0900, Souma wrote:
> diff --git a/builtin/history.c b/builtin/history.c
> index 091465a59e..8d669cf539 100644
> --- a/builtin/history.c
> +++ b/builtin/history.c
> @@ -98,6 +98,30 @@ enum commit_tree_flags {
> COMMIT_TREE_EDIT_MESSAGE = (1 << 0),
> };
>
> +static int history_config(const char *var, const char *value,
> + const struct config_context *ctx, void *data)
> +{
> + const char **sign_commit = data;
> +
> + if (!strcmp(var, "commit.gpgsign")) {
> + *sign_commit = git_config_bool(var, value) ? "" : NULL;
> + return 0;
> + }
> +
> + return git_default_config(var, value, ctx, data);
Shouldn't we rather pass `NULL` instead of `data`? It works, sure, but
only because `git_default_config()` doesn't use `data` at all.
> @@ -160,7 +185,8 @@ static int commit_tree_ext(struct repository *repo,
> static int commit_tree_with_edited_message(struct repository *repo,
> const char *action,
> struct commit *original,
> - struct commit **out)
> + struct commit **out,
> + const char *sign_commit)
Nit: the `out` parameter should continue to be the last one.
> @@ -515,12 +546,13 @@ static int cmd_history_fixup(int argc,
> bool skip_commit = false;
> int ret;
>
> + repo_config(repo, history_config, &sign_commit);
> +
> argc = parse_options(argc, argv, prefix, options, usage, 0);
> if (argc != 1) {
> ret = error(_("command expects a single revision"));
> goto out;
> }
> - repo_config(repo, git_default_config, NULL);
>
> if (action == REF_ACTION_DEFAULT)
> action = REF_ACTION_BRANCHES;
It might make sense to document in the commit message why we have to
change the order. I guess it's because of precedence, but not everyone
might realize that immediately.
> @@ -785,7 +822,8 @@ static int write_ondisk_index(struct repository *repo,
> static int split_commit(struct repository *repo,
> struct commit *original,
> struct pathspec *pathspec,
> - struct commit **out)
> + struct commit **out,
> + const char *sign_commit)
> {
> struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
> struct strbuf index_file = STRBUF_INIT;
Likewise, let's ensure that the `out` parameter remains last.
> diff --git a/replay.c b/replay.c
> index da531d5bc6..683c384ef8 100644
> --- a/replay.c
> +++ b/replay.c
It might make sense to split out the changes to "replay.c" into a
preparatory commit.
One interesting question is whether it really makes sense to sign _all_
commits. It's rather likely that the history will contain commits that
aren't even owned by you, so signing them with your signature might be a
bit of a weird choice. I guess that might be okay-ish, but it's
certainly something that's worth a discussion as part of the commit
message.
Thanks!
Patrick
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v2 0/2] history: support signing rewritten commits
2026-07-03 14:50 [PATCH 0/3] history: sign rewritten commits Souma
` (2 preceding siblings ...)
2026-07-03 14:50 ` [PATCH 3/3] t345x: cover signed history rewrites Souma
@ 2026-07-17 14:51 ` Souma
2026-07-17 14:51 ` [PATCH v2 1/2] replay: allow callers to sign commits Souma
2026-07-17 14:51 ` [PATCH v2 2/2] builtin/history: sign rewritten commits Souma
5 siblings, 0 replies; 9+ messages in thread
From: Souma @ 2026-07-17 14:51 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Souma
The history commands create commits directly and via the replay
machinery, but currently have no way to honor `commit.gpgSign` or an
explicit signing request. This means users who require signed commits
lose that property when rewriting history.
Teach the replay API to accept a signing key, then expose the standard
`-S`/`--gpg-sign[=<key-id>]` and `--no-gpg-sign` interface across the
`history drop`, `history fixup`, `history reword`, and `history split`
subcommands. The selected policy applies to every new commit, including
both halves of a split and replayed descendants. A drop of the tip
creates no replacement commit and therefore has nothing to sign.
The implementation follows the precedence used by rebase, cherry-pick,
and revert: `commit.gpgSign` supplies the default, command-line options
override it, and the last command-line option wins.
The signature records the attestation of the current committer to the
rewritten commit while retaining the original author identity; it does
not claim authorship of commits written by somebody else.
Changes since v1:
- Split the replay signing plumbing into a preparatory patch
- Fold the documentation and tests into the feature patch so each
commit builds and passes t0450
- Move `sign_commit` before the output parameter of `commit_tree_ext()` and
update its callers accordingly
- Pass `NULL` to `git_default_config()`
- Document why configuration is read before command-line options
- Clarify that every rewritten commit is signed, including commits with
a different author
- Add signing support and tests for the new `history drop` subcommand
- Add coverage for selecting an explicit signing key
Souma (2):
replay: allow callers to sign commits
builtin/history: sign rewritten commits
Documentation/git-history.adoc | 16 +++++--
builtin/history.c | 84 ++++++++++++++++++++++++++--------
replay.c | 13 ++++--
replay.h | 6 +++
t/t3451-history-reword.sh | 63 +++++++++++++++++++++++++
t/t3452-history-split.sh | 44 ++++++++++++++++++
t/t3453-history-fixup.sh | 39 ++++++++++++++++
t/t3454-history-drop.sh | 50 ++++++++++++++++++++
8 files changed, 286 insertions(+), 29 deletions(-)
Range-diff against v1:
1: 60f7c13514 ! 1: 3f4dc0b982 builtin/history: sign rewritten commits
@@ Metadata
Author: Souma <git@5ouma.me>
## Commit message ##
- builtin/history: sign rewritten commits
+ replay: allow callers to sign commits
- The history commands create replacement commits directly instead of
- using the sequencer or the commit porcelain. As a result, rewritten
- commits ignore commit.gpgsign and cannot be signed on demand.
+ The replay machinery creates commits directly through
+ `commit_tree_extended()`, but callers cannot currently request
+ signatures. Commands that replay rewritten history consequently cannot
+ carry their signing policy through to descendant commits.
- Read the usual signing configuration before parsing history options.
- Add the commit-style -S/--gpg-sign knob, and pass the selected
- signing key through direct rewrites and replayed descendants.
+ Add `sign_commit` to `replay_revisions_options` and thread it through
+ commit creation. `NULL` preserves the existing unsigned behavior, an
+ empty string selects the default signing key, and a non-empty string
+ selects an explicit key. Existing callers zero-initialize the options
+ structure, so their behavior is unchanged.
Signed-off-by: Souma <git@5ouma.me>
- ## builtin/history.c ##
-@@
- #include "wt-status.h"
-
- #define GIT_HISTORY_FIXUP_USAGE \
-- N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]")
-+ N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]")
- #define GIT_HISTORY_REWORD_USAGE \
-- N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]")
-+ N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]")
- #define GIT_HISTORY_SPLIT_USAGE \
-- N_("git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]")
-+ N_("git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]")
-
- static void change_data_free(void *util, const char *str UNUSED)
- {
-@@ builtin/history.c: enum commit_tree_flags {
- COMMIT_TREE_EDIT_MESSAGE = (1 << 0),
- };
-
-+static int history_config(const char *var, const char *value,
-+ const struct config_context *ctx, void *data)
-+{
-+ const char **sign_commit = data;
-+
-+ if (!strcmp(var, "commit.gpgsign")) {
-+ *sign_commit = git_config_bool(var, value) ? "" : NULL;
-+ return 0;
-+ }
-+
-+ return git_default_config(var, value, ctx, data);
-+}
-+
-+#define OPT_HISTORY_GPG_SIGN(v) { \
-+ .type = OPTION_STRING, \
-+ .short_name = 'S', \
-+ .long_name = "gpg-sign", \
-+ .value = (v), \
-+ .argh = N_("key-id"), \
-+ .help = N_("GPG-sign rewritten commits"), \
-+ .flags = PARSE_OPT_OPTARG, \
-+ .defval = (intptr_t) "", \
-+}
-+
- static int commit_tree_ext(struct repository *repo,
- const char *action,
- struct commit *commit_with_message,
-@@ builtin/history.c: static int commit_tree_ext(struct repository *repo,
- const struct object_id *old_tree,
- const struct object_id *new_tree,
- struct commit **out,
-+ const char *sign_commit,
- enum commit_tree_flags flags)
- {
- const char *exclude_gpgsig[] = {
-@@ builtin/history.c: static int commit_tree_ext(struct repository *repo,
-
- ret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree,
- parents, &rewritten_commit_oid, original_author,
-- NULL, NULL, original_extra_headers);
-+ NULL, sign_commit, original_extra_headers);
- if (ret < 0)
- goto out;
-
-@@ builtin/history.c: static int commit_tree_ext(struct repository *repo,
- static int commit_tree_with_edited_message(struct repository *repo,
- const char *action,
- struct commit *original,
-- struct commit **out)
-+ struct commit **out,
-+ const char *sign_commit)
- {
- struct object_id parent_tree_oid;
- const struct object_id *tree_oid;
-@@ builtin/history.c: static int commit_tree_with_edited_message(struct repository *repo,
- }
-
- return commit_tree_ext(repo, action, original, original->parents,
-- &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);
-+ &parent_tree_oid, tree_oid, out, sign_commit,
-+ COMMIT_TREE_EDIT_MESSAGE);
- }
-
- enum ref_action {
-@@ builtin/history.c: static int handle_reference_updates(struct rev_info *revs,
- struct commit *rewritten,
- const char *reflog_msg,
- int dry_run,
-+ const char *sign_commit,
- enum replay_empty_commit_action empty)
- {
- const struct name_decoration *decoration;
- struct replay_revisions_options opts = {
- .empty = empty,
-+ .sign_commit = sign_commit,
- };
- struct replay_result result = { 0 };
- struct ref_transaction *transaction = NULL;
-@@ builtin/history.c: static int cmd_history_fixup(int argc,
- enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;
- enum ref_action action = REF_ACTION_DEFAULT;
- enum commit_tree_flags flags = 0;
-+ const char *sign_commit = NULL;
- int dry_run = 0;
- struct option options[] = {
- OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
-@@ builtin/history.c: static int cmd_history_fixup(int argc,
- OPT_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)",
- N_("how to handle commits that become empty"),
- PARSE_OPT_NONEG, parse_opt_empty),
-+ OPT_HISTORY_GPG_SIGN(&sign_commit),
- OPT_END(),
- };
- struct merge_result merge_result = { 0 };
-@@ builtin/history.c: static int cmd_history_fixup(int argc,
- bool skip_commit = false;
- int ret;
-
-+ repo_config(repo, history_config, &sign_commit);
-+
- argc = parse_options(argc, argv, prefix, options, usage, 0);
- if (argc != 1) {
- ret = error(_("command expects a single revision"));
- goto out;
- }
-- repo_config(repo, git_default_config, NULL);
-
- if (action == REF_ACTION_DEFAULT)
- action = REF_ACTION_BRANCHES;
-@@ builtin/history.c: static int cmd_history_fixup(int argc,
- if (!skip_commit) {
- ret = commit_tree_ext(repo, "fixup", original, original->parents,
- &original_tree->object.oid, &merge_result.tree->object.oid,
-- &rewritten, flags);
-+ &rewritten, sign_commit, flags);
- if (ret < 0) {
- ret = error(_("failed writing fixed-up commit"));
- goto out;
-@@ builtin/history.c: static int cmd_history_fixup(int argc,
- strbuf_addf(&reflog_msg, "fixup: updating %s", argv[0]);
-
- ret = handle_reference_updates(&revs, action, original, rewritten,
-- reflog_msg.buf, dry_run, empty);
-+ reflog_msg.buf, dry_run, sign_commit, empty);
- if (ret < 0) {
- ret = error(_("failed replaying descendants"));
- goto out;
-@@ builtin/history.c: static int cmd_history_reword(int argc,
- NULL,
- };
- enum ref_action action = REF_ACTION_DEFAULT;
-+ const char *sign_commit = NULL;
- int dry_run = 0;
- struct option options[] = {
- OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
-@@ builtin/history.c: static int cmd_history_reword(int argc,
- PARSE_OPT_NONEG, parse_ref_action),
- OPT_BOOL('n', "dry-run", &dry_run,
- N_("perform a dry-run without updating any refs")),
-+ OPT_HISTORY_GPG_SIGN(&sign_commit),
- OPT_END(),
- };
- struct strbuf reflog_msg = STRBUF_INIT;
-@@ builtin/history.c: static int cmd_history_reword(int argc,
- struct rev_info revs = { 0 };
- int ret;
-
-+ repo_config(repo, history_config, &sign_commit);
-+
- argc = parse_options(argc, argv, prefix, options, usage, 0);
- if (argc != 1) {
- ret = error(_("command expects a single revision"));
- goto out;
- }
-- repo_config(repo, git_default_config, NULL);
-
- if (action == REF_ACTION_DEFAULT)
- action = REF_ACTION_BRANCHES;
-@@ builtin/history.c: static int cmd_history_reword(int argc,
- if (ret)
- goto out;
-
-- ret = commit_tree_with_edited_message(repo, "reworded", original, &rewritten);
-+ ret = commit_tree_with_edited_message(repo, "reworded", original,
-+ &rewritten, sign_commit);
- if (ret < 0) {
- ret = error(_("failed writing reworded commit"));
- goto out;
-@@ builtin/history.c: static int cmd_history_reword(int argc,
- strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
-
- ret = handle_reference_updates(&revs, action, original, rewritten,
-- reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
-+ reflog_msg.buf, dry_run, sign_commit,
-+ REPLAY_EMPTY_COMMIT_ABORT);
- if (ret < 0) {
- ret = error(_("failed replaying descendants"));
- goto out;
-@@ builtin/history.c: static int write_ondisk_index(struct repository *repo,
- static int split_commit(struct repository *repo,
- struct commit *original,
- struct pathspec *pathspec,
-- struct commit **out)
-+ struct commit **out,
-+ const char *sign_commit)
- {
- struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
- struct strbuf index_file = STRBUF_INIT;
-@@ builtin/history.c: static int split_commit(struct repository *repo,
- * that shall be diffed against is the parent of the original commit.
- */
- ret = commit_tree_ext(repo, "split-out", original, original->parents, &parent_tree_oid,
-- &split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);
-+ &split_tree->object.oid, &first_commit, sign_commit,
-+ COMMIT_TREE_EDIT_MESSAGE);
- if (ret < 0) {
- ret = error(_("failed writing first commit"));
- goto out;
-@@ builtin/history.c: static int split_commit(struct repository *repo,
- new_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;
-
- ret = commit_tree_ext(repo, "split-out", original, parents, old_tree_oid,
-- new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);
-+ new_tree_oid, &second_commit, sign_commit,
-+ COMMIT_TREE_EDIT_MESSAGE);
- if (ret < 0) {
- ret = error(_("failed writing second commit"));
- goto out;
-@@ builtin/history.c: static int cmd_history_split(int argc,
- NULL,
- };
- enum ref_action action = REF_ACTION_DEFAULT;
-+ const char *sign_commit = NULL;
- int dry_run = 0;
- struct option options[] = {
- OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
-@@ builtin/history.c: static int cmd_history_split(int argc,
- PARSE_OPT_NONEG, parse_ref_action),
- OPT_BOOL('n', "dry-run", &dry_run,
- N_("perform a dry-run without updating any refs")),
-+ OPT_HISTORY_GPG_SIGN(&sign_commit),
- OPT_END(),
- };
- struct commit *original, *rewritten = NULL;
-@@ builtin/history.c: static int cmd_history_split(int argc,
- struct rev_info revs = { 0 };
- int ret;
-
-+ repo_config(repo, history_config, &sign_commit);
-+
- argc = parse_options(argc, argv, prefix, options, usage, 0);
- if (argc < 1) {
- ret = error(_("command expects a committish"));
- goto out;
- }
-- repo_config(repo, git_default_config, NULL);
-
- if (action == REF_ACTION_DEFAULT)
- action = REF_ACTION_BRANCHES;
-@@ builtin/history.c: static int cmd_history_split(int argc,
- goto out;
- }
-
-- ret = split_commit(repo, original, &pathspec, &rewritten);
-+ ret = split_commit(repo, original, &pathspec, &rewritten, sign_commit);
- if (ret < 0)
- goto out;
-
- strbuf_addf(&reflog_msg, "split: updating %s", argv[0]);
-
- ret = handle_reference_updates(&revs, action, original, rewritten,
-- reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
-+ reflog_msg.buf, dry_run, sign_commit,
-+ REPLAY_EMPTY_COMMIT_ABORT);
- if (ret < 0) {
- ret = error(_("failed replaying descendants"));
- goto out;
-
## replay.c ##
@@ replay.c: static struct commit *create_commit(struct repository *repo,
struct tree *tree,
2: 9935928b01 < -: ---------- doc: document history signing options
3: c017e90034 ! 2: 0e63c0b66a t345x: cover signed history rewrites
@@ Metadata
Author: Souma <git@5ouma.me>
## Commit message ##
- t345x: cover signed history rewrites
+ builtin/history: sign rewritten commits
- History signing needs regression coverage because these commands bypass the
- usual commit machinery and create replacement commits through lower-level
- APIs.
+ The history commands create replacement commits directly instead of
+ using the sequencer or the commit porcelain. As a result, rewritten
+ commits ignore `commit.gpgSign` and cannot be signed on demand.
- Add GPG-gated tests for config-driven signing, command-line signing,
- --no-gpg-sign precedence, and signing of replayed descendants after fixup,
- reword, and split.
+ Read the signing configuration before parsing options so that it
+ establishes the default and later `-S`/`--gpg-sign` or `--no-gpg-sign`
+ options override it. Pass the selected key through direct rewrites and
+ the replay machinery.
+
+ Sign every newly created commit, including both halves of a split and
+ replayed descendants. Dropping the tip creates no replacement commit,
+ so there is nothing to sign. As with `rebase --gpg-sign`, the signature
+ records the attestation of the current committer to the rewritten
+ commit while retaining the original author identity; it does not claim
+ authorship of commits written by somebody else.
+
+ Document the behavior and add GPG-gated coverage for configuration,
+ command-line overrides, last-option-wins precedence, replayed
+ descendants, split commits, an explicit signing key, and the
+ no-new-commit drop case.
Signed-off-by: Souma <git@5ouma.me>
+ ## Documentation/git-history.adoc ##
+@@ Documentation/git-history.adoc: git-history - EXPERIMENTAL: Rewrite history
+ SYNOPSIS
+ --------
+ [synopsis]
+-git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]
+-git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]
+-git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
+-git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]
++git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]
++git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]
++git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]
++git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]
+
+ DESCRIPTION
+ -----------
+@@ Documentation/git-history.adoc: OPTIONS
+ `--reedit-message`::
+ Open an editor to modify the target commit's message.
+
++`-S[<key-id>]`::
++`--gpg-sign[=<key-id>]`::
++`--no-gpg-sign`::
++ GPG-sign rewritten commits. The _<key-id>_ argument is optional and
++ defaults to the committer identity; if specified, it must be stuck to
++ the option without a space. `--no-gpg-sign` is useful to countermand
++ both `commit.gpgSign` configuration and earlier `--gpg-sign`.
++
+ `--empty=(drop|keep|abort)`::
+ Control what happens when a commit becomes empty as a result of the
+ fixup. This can happen in two situations:
+
+ ## builtin/history.c ##
+@@
+ #include "wt-status.h"
+
+ #define GIT_HISTORY_DROP_USAGE \
+- N_("git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]")
++ N_("git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]")
+ #define GIT_HISTORY_FIXUP_USAGE \
+- N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]")
++ N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]")
+ #define GIT_HISTORY_REWORD_USAGE \
+- N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]")
++ N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]")
+ #define GIT_HISTORY_SPLIT_USAGE \
+- N_("git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]")
++ N_("git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]")
+
+ static void change_data_free(void *util, const char *str UNUSED)
+ {
+@@ builtin/history.c: enum commit_tree_flags {
+ COMMIT_TREE_EDIT_MESSAGE = (1 << 0),
+ };
+
++static int history_config(const char *var, const char *value,
++ const struct config_context *ctx, void *data)
++{
++ const char **sign_commit = data;
++
++ if (!strcmp(var, "commit.gpgsign")) {
++ *sign_commit = git_config_bool(var, value) ? "" : NULL;
++ return 0;
++ }
++
++ return git_default_config(var, value, ctx, NULL);
++}
++
++#define OPT_HISTORY_GPG_SIGN(v) { \
++ .type = OPTION_STRING, \
++ .short_name = 'S', \
++ .long_name = "gpg-sign", \
++ .value = (v), \
++ .argh = N_("key-id"), \
++ .help = N_("GPG-sign rewritten commits"), \
++ .flags = PARSE_OPT_OPTARG, \
++ .defval = (intptr_t)"", \
++}
++
+ static int commit_tree_ext(struct repository *repo,
+ const char *action,
+ struct commit *commit_with_message,
+ const struct commit_list *parents,
+ const struct object_id *old_tree,
+ const struct object_id *new_tree,
++ const char *sign_commit,
+ struct commit **out,
+ enum commit_tree_flags flags)
+ {
+@@ builtin/history.c: static int commit_tree_ext(struct repository *repo,
+
+ ret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree,
+ parents, &rewritten_commit_oid, original_author,
+- NULL, NULL, original_extra_headers);
++ NULL, sign_commit, original_extra_headers);
+ if (ret < 0)
+ goto out;
+
+@@ builtin/history.c: static int commit_tree_ext(struct repository *repo,
+ static int commit_tree_with_edited_message(struct repository *repo,
+ const char *action,
+ struct commit *original,
++ const char *sign_commit,
+ struct commit **out)
+ {
+ struct object_id parent_tree_oid;
+@@ builtin/history.c: static int commit_tree_with_edited_message(struct repository *repo,
+ }
+
+ return commit_tree_ext(repo, action, original, original->parents,
+- &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);
++ &parent_tree_oid, tree_oid, sign_commit, out,
++ COMMIT_TREE_EDIT_MESSAGE);
+ }
+
+ enum ref_action {
+@@ builtin/history.c: static int compute_pending_ref_updates(struct rev_info *revs,
+ enum ref_action action,
+ struct commit *original,
+ struct commit *rewritten,
++ const char *sign_commit,
+ enum replay_empty_commit_action empty,
+ struct replay_result *result)
+ {
+ const struct name_decoration *decoration;
+ struct replay_revisions_options opts = {
+ .empty = empty,
++ .sign_commit = sign_commit,
+ };
+ char hex[GIT_MAX_HEXSZ + 1];
+ bool detached_head;
+@@ builtin/history.c: static int handle_reference_updates(struct rev_info *revs,
+ struct commit *rewritten,
+ const char *reflog_msg,
+ int dry_run,
++ const char *sign_commit,
+ enum replay_empty_commit_action empty)
+ {
+ struct replay_result result = { 0 };
+ int ret;
+
+ ret = compute_pending_ref_updates(revs, action, original, rewritten,
+- empty, &result);
++ sign_commit, empty, &result);
+ if (ret)
+ goto out;
+
+@@ builtin/history.c: static int cmd_history_fixup(int argc,
+ enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;
+ enum ref_action action = REF_ACTION_DEFAULT;
+ enum commit_tree_flags flags = 0;
++ const char *sign_commit = NULL;
+ int dry_run = 0;
+ struct option options[] = {
+ OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
+@@ builtin/history.c: static int cmd_history_fixup(int argc,
+ OPT_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)",
+ N_("how to handle commits that become empty"),
+ PARSE_OPT_NONEG, parse_opt_empty),
++ OPT_HISTORY_GPG_SIGN(&sign_commit),
+ OPT_END(),
+ };
+ struct merge_result merge_result = { 0 };
+@@ builtin/history.c: static int cmd_history_fixup(int argc,
+ bool skip_commit = false;
+ int ret;
+
++ repo_config(repo, history_config, &sign_commit);
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc != 1) {
+ ret = error(_("command expects a single revision"));
+ goto out;
+ }
+- repo_config(repo, git_default_config, NULL);
+
+ if (action == REF_ACTION_DEFAULT)
+ action = REF_ACTION_BRANCHES;
+@@ builtin/history.c: static int cmd_history_fixup(int argc,
+ if (!skip_commit) {
+ ret = commit_tree_ext(repo, "fixup", original, original->parents,
+ &original_tree->object.oid, &merge_result.tree->object.oid,
+- &rewritten, flags);
++ sign_commit, &rewritten, flags);
+ if (ret < 0) {
+ ret = error(_("failed writing fixed-up commit"));
+ goto out;
+@@ builtin/history.c: static int cmd_history_fixup(int argc,
+ strbuf_addf(&reflog_msg, "fixup: updating %s", argv[0]);
+
+ ret = handle_reference_updates(&revs, action, original, rewritten,
+- reflog_msg.buf, dry_run, empty);
++ reflog_msg.buf, dry_run, sign_commit, empty);
+ if (ret < 0) {
+ ret = error(_("failed replaying descendants"));
+ goto out;
+@@ builtin/history.c: static int cmd_history_reword(int argc,
+ NULL,
+ };
+ enum ref_action action = REF_ACTION_DEFAULT;
++ const char *sign_commit = NULL;
+ int dry_run = 0;
+ struct option options[] = {
+ OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
+@@ builtin/history.c: static int cmd_history_reword(int argc,
+ PARSE_OPT_NONEG, parse_ref_action),
+ OPT_BOOL('n', "dry-run", &dry_run,
+ N_("perform a dry-run without updating any refs")),
++ OPT_HISTORY_GPG_SIGN(&sign_commit),
+ OPT_END(),
+ };
+ struct strbuf reflog_msg = STRBUF_INIT;
+@@ builtin/history.c: static int cmd_history_reword(int argc,
+ struct rev_info revs = { 0 };
+ int ret;
+
++ repo_config(repo, history_config, &sign_commit);
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc != 1) {
+ ret = error(_("command expects a single revision"));
+ goto out;
+ }
+- repo_config(repo, git_default_config, NULL);
+
+ if (action == REF_ACTION_DEFAULT)
+ action = REF_ACTION_BRANCHES;
+@@ builtin/history.c: static int cmd_history_reword(int argc,
+ if (ret)
+ goto out;
+
+- ret = commit_tree_with_edited_message(repo, "reworded", original, &rewritten);
++ ret = commit_tree_with_edited_message(repo, "reworded", original,
++ sign_commit, &rewritten);
+ if (ret < 0) {
+ ret = error(_("failed writing reworded commit"));
+ goto out;
+@@ builtin/history.c: static int cmd_history_reword(int argc,
+ strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
+
+ ret = handle_reference_updates(&revs, action, original, rewritten,
+- reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
++ reflog_msg.buf, dry_run, sign_commit,
++ REPLAY_EMPTY_COMMIT_ABORT);
+ if (ret < 0) {
+ ret = error(_("failed replaying descendants"));
+ goto out;
+@@ builtin/history.c: static int write_ondisk_index(struct repository *repo,
+ static int split_commit(struct repository *repo,
+ struct commit *original,
+ struct pathspec *pathspec,
++ const char *sign_commit,
+ struct commit **out)
+ {
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
+@@ builtin/history.c: static int split_commit(struct repository *repo,
+ * that shall be diffed against is the parent of the original commit.
+ */
+ ret = commit_tree_ext(repo, "split-out", original, original->parents, &parent_tree_oid,
+- &split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);
++ &split_tree->object.oid, sign_commit, &first_commit,
++ COMMIT_TREE_EDIT_MESSAGE);
+ if (ret < 0) {
+ ret = error(_("failed writing first commit"));
+ goto out;
+@@ builtin/history.c: static int split_commit(struct repository *repo,
+ new_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;
+
+ ret = commit_tree_ext(repo, "split-out", original, parents, old_tree_oid,
+- new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);
++ new_tree_oid, sign_commit, &second_commit,
++ COMMIT_TREE_EDIT_MESSAGE);
+ if (ret < 0) {
+ ret = error(_("failed writing second commit"));
+ goto out;
+@@ builtin/history.c: static int cmd_history_split(int argc,
+ NULL,
+ };
+ enum ref_action action = REF_ACTION_DEFAULT;
++ const char *sign_commit = NULL;
+ int dry_run = 0;
+ struct option options[] = {
+ OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
+@@ builtin/history.c: static int cmd_history_split(int argc,
+ PARSE_OPT_NONEG, parse_ref_action),
+ OPT_BOOL('n', "dry-run", &dry_run,
+ N_("perform a dry-run without updating any refs")),
++ OPT_HISTORY_GPG_SIGN(&sign_commit),
+ OPT_END(),
+ };
+ struct commit *original, *rewritten = NULL;
+@@ builtin/history.c: static int cmd_history_split(int argc,
+ struct rev_info revs = { 0 };
+ int ret;
+
++ repo_config(repo, history_config, &sign_commit);
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc < 1) {
+ ret = error(_("command expects a committish"));
+ goto out;
+ }
+- repo_config(repo, git_default_config, NULL);
+
+ if (action == REF_ACTION_DEFAULT)
+ action = REF_ACTION_BRANCHES;
+@@ builtin/history.c: static int cmd_history_split(int argc,
+ goto out;
+ }
+
+- ret = split_commit(repo, original, &pathspec, &rewritten);
++ ret = split_commit(repo, original, &pathspec, sign_commit, &rewritten);
+ if (ret < 0)
+ goto out;
+
+ strbuf_addf(&reflog_msg, "split: updating %s", argv[0]);
+
+ ret = handle_reference_updates(&revs, action, original, rewritten,
+- reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
++ reflog_msg.buf, dry_run, sign_commit,
++ REPLAY_EMPTY_COMMIT_ABORT);
+ if (ret < 0) {
+ ret = error(_("failed replaying descendants"));
+ goto out;
+@@ builtin/history.c: static int cmd_history_drop(int argc,
+ };
+ enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;
+ enum ref_action action = REF_ACTION_DEFAULT;
++ const char *sign_commit = NULL;
+ int dry_run = 0;
+ struct option options[] = {
+ OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
+@@ builtin/history.c: static int cmd_history_drop(int argc,
+ OPT_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)",
+ N_("how to handle descendants that become empty"),
+ PARSE_OPT_NONEG, parse_opt_empty),
++ OPT_HISTORY_GPG_SIGN(&sign_commit),
+ OPT_END(),
+ };
+ struct strbuf reflog_msg = STRBUF_INIT;
+@@ builtin/history.c: static int cmd_history_drop(int argc,
+ bool head_moves = false;
+ int ret;
+
++ repo_config(repo, history_config, &sign_commit);
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc != 1) {
+ ret = error(_("command expects a single revision"));
+ goto out;
+ }
+- repo_config(repo, git_default_config, NULL);
+
+ if (action == REF_ACTION_DEFAULT)
+ action = REF_ACTION_BRANCHES;
+@@ builtin/history.c: static int cmd_history_drop(int argc,
+ rewritten = original->parents->item;
+
+ ret = compute_pending_ref_updates(&revs, action, original, rewritten,
+- empty, &result);
++ sign_commit, empty, &result);
+ if (ret) {
+ ret = error(_("failed replaying descendants"));
+ goto out;
+
## t/t3451-history-reword.sh ##
@@ t/t3451-history-reword.sh: test_description='tests for git-history reword subcommand'
@@ t/t3451-history-reword.sh: test_expect_success 'can reword commit in the middle'
+test_reword_gpg_sign ! true --no-gpg-sign
+test_reword_gpg_sign ! true --gpg-sign --no-gpg-sign
+test_reword_gpg_sign false --no-gpg-sign --gpg-sign
++
++test_expect_success GPG 'reword uses an explicit signing key for rewritten history' '
++ test_when_finished "rm -rf repo" &&
++ git init repo &&
++ (
++ cd repo &&
++ test_commit first &&
++ test_commit second &&
++ test_commit third &&
++
++ reword_with_message -SB7227189 HEAD~ <<-EOF &&
++ second reworded
++ EOF
++
++ git verify-commit HEAD~ &&
++ git verify-commit HEAD &&
++ git log -2 --format=%GK >actual &&
++ cat >expect <<-\EOF &&
++ 65A0EEA02E30CAD7
++ 65A0EEA02E30CAD7
++ EOF
++ test_cmp expect actual
++ )
++'
+
test_expect_success 'can reword commit in the middle even on detached head' '
test_when_finished "rm -rf repo" &&
@@ t/t3453-history-fixup.sh: test_expect_success 'preserves commit message and auth
test_expect_success 'updates all descendant branches by default' '
test_when_finished "rm -rf repo" &&
git init repo --initial-branch=main &&
+
+ ## t/t3454-history-drop.sh ##
+@@ t/t3454-history-drop.sh: test_description='tests for git-history drop subcommand'
+
+ . ./test-lib.sh
+ . "$TEST_DIRECTORY/lib-log-graph.sh"
++. "$TEST_DIRECTORY/lib-gpg.sh"
+
+ expect_graph () {
+ cat >expect &&
+@@ t/t3454-history-drop.sh: expect_log () {
+ test_cmp expect actual
+ }
+
++test_drop_gpg_sign () {
++ must_fail= will=will
++ if test "x$1" = "x!"
++ then
++ must_fail=test_must_fail
++ will="will not"
++ shift
++ fi
++ conf=$1
++ shift
++
++ test_expect_success GPG "drop $* with commit.gpgsign=$conf $will sign replayed descendants" "
++ test_when_finished 'rm -rf repo' &&
++ git init repo &&
++ (
++ cd repo &&
++ test_commit first &&
++ test_commit second &&
++ test_commit third &&
++
++ git config commit.gpgsign $conf &&
++ git history drop $* HEAD~ &&
++
++ $must_fail git verify-commit HEAD
++ )
++ "
++}
++
+ test_expect_success 'errors on missing commit argument' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+@@ t/t3454-history-drop.sh: test_expect_success 'drops a commit in the middle and replays descendants' '
+ )
+ '
+
++test_drop_gpg_sign ! false
++test_drop_gpg_sign true
++test_drop_gpg_sign false --gpg-sign
++test_drop_gpg_sign ! true --no-gpg-sign
++test_drop_gpg_sign ! true --gpg-sign --no-gpg-sign
++test_drop_gpg_sign false --no-gpg-sign --gpg-sign
++
++test_expect_success GPG 'drop has no commit to sign when dropping the tip' '
++ test_when_finished "rm -rf repo" &&
++ git init repo &&
++ (
++ cd repo &&
++ test_commit first &&
++ test_commit second &&
++
++ git history drop --gpg-sign HEAD &&
++
++ test_must_fail git verify-commit HEAD
++ )
++'
++
+ test_expect_success 'drops the HEAD commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
--
2.55.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v2 1/2] replay: allow callers to sign commits
2026-07-03 14:50 [PATCH 0/3] history: sign rewritten commits Souma
` (3 preceding siblings ...)
2026-07-17 14:51 ` [PATCH v2 0/2] history: support signing rewritten commits Souma
@ 2026-07-17 14:51 ` Souma
2026-07-17 14:51 ` [PATCH v2 2/2] builtin/history: sign rewritten commits Souma
5 siblings, 0 replies; 9+ messages in thread
From: Souma @ 2026-07-17 14:51 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Souma
The replay machinery creates commits directly through
`commit_tree_extended()`, but callers cannot currently request
signatures. Commands that replay rewritten history consequently cannot
carry their signing policy through to descendant commits.
Add `sign_commit` to `replay_revisions_options` and thread it through
commit creation. `NULL` preserves the existing unsigned behavior, an
empty string selects the default signing key, and a non-empty string
selects an explicit key. Existing callers zero-initialize the options
structure, so their behavior is unchanged.
Signed-off-by: Souma <git@5ouma.me>
---
replay.c | 13 ++++++++-----
replay.h | 6 ++++++
2 files changed, 14 insertions(+), 5 deletions(-)
diff --git a/replay.c b/replay.c
index aac9178875..19a6402bf0 100644
--- a/replay.c
+++ b/replay.c
@@ -81,13 +81,13 @@ static struct commit *create_commit(struct repository *repo,
struct tree *tree,
struct commit *based_on,
struct commit *parent,
- enum replay_mode mode)
+ enum replay_mode mode,
+ const char *sign_commit)
{
struct object_id ret;
struct object *obj = NULL;
struct commit_list *parents = NULL;
char *author = NULL;
- char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
struct commit_extra_header *extra = NULL;
struct strbuf msg = STRBUF_INIT;
const char *out_enc = get_commit_output_encoding();
@@ -270,7 +270,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
struct merge_options *merge_opt,
struct merge_result *result,
enum replay_mode mode,
- enum replay_empty_commit_action empty)
+ enum replay_empty_commit_action empty,
+ const char *sign_commit)
{
struct commit *base, *replayed_base;
struct tree *pickme_tree, *base_tree, *replayed_base_tree;
@@ -341,7 +342,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
}
}
- return create_commit(repo, result->tree, pickme, replayed_base, mode);
+ return create_commit(repo, result->tree, pickme, replayed_base, mode,
+ sign_commit);
}
void replay_result_release(struct replay_result *result)
@@ -431,7 +433,8 @@ int replay_revisions(struct rev_info *revs,
last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
mode == REPLAY_MODE_REVERT ? last_commit : onto,
- &merge_opt, &result, mode, opts->empty);
+ &merge_opt, &result, mode, opts->empty,
+ opts->sign_commit);
if (!last_commit)
break;
diff --git a/replay.h b/replay.h
index 491db145e3..6ed0608911 100644
--- a/replay.h
+++ b/replay.h
@@ -57,6 +57,12 @@ struct replay_revisions_options {
*/
int contained;
+ /*
+ * Key used to sign newly-created commits. An empty string requests the
+ * default configured signing key, and NULL disables signing.
+ */
+ const char *sign_commit;
+
/*
* Controls what to do when a replayed commit becomes empty.
* Defaults to REPLAY_EMPTY_COMMIT_DROP.
--
2.55.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v2 2/2] builtin/history: sign rewritten commits
2026-07-03 14:50 [PATCH 0/3] history: sign rewritten commits Souma
` (4 preceding siblings ...)
2026-07-17 14:51 ` [PATCH v2 1/2] replay: allow callers to sign commits Souma
@ 2026-07-17 14:51 ` Souma
5 siblings, 0 replies; 9+ messages in thread
From: Souma @ 2026-07-17 14:51 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Souma
The history commands create replacement commits directly instead of
using the sequencer or the commit porcelain. As a result, rewritten
commits ignore `commit.gpgSign` and cannot be signed on demand.
Read the signing configuration before parsing options so that it
establishes the default and later `-S`/`--gpg-sign` or `--no-gpg-sign`
options override it. Pass the selected key through direct rewrites and
the replay machinery.
Sign every newly created commit, including both halves of a split and
replayed descendants. Dropping the tip creates no replacement commit,
so there is nothing to sign. As with `rebase --gpg-sign`, the signature
records the attestation of the current committer to the rewritten
commit while retaining the original author identity; it does not claim
authorship of commits written by somebody else.
Document the behavior and add GPG-gated coverage for configuration,
command-line overrides, last-option-wins precedence, replayed
descendants, split commits, an explicit signing key, and the
no-new-commit drop case.
Signed-off-by: Souma <git@5ouma.me>
---
Documentation/git-history.adoc | 16 +++++--
builtin/history.c | 84 ++++++++++++++++++++++++++--------
t/t3451-history-reword.sh | 63 +++++++++++++++++++++++++
t/t3452-history-split.sh | 44 ++++++++++++++++++
t/t3453-history-fixup.sh | 39 ++++++++++++++++
t/t3454-history-drop.sh | 50 ++++++++++++++++++++
6 files changed, 272 insertions(+), 24 deletions(-)
diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc
index 28b477cd37..8345cced4c 100644
--- a/Documentation/git-history.adoc
+++ b/Documentation/git-history.adoc
@@ -8,10 +8,10 @@ git-history - EXPERIMENTAL: Rewrite history
SYNOPSIS
--------
[synopsis]
-git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]
-git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]
-git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
-git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]
+git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]
+git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]
+git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]
+git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]
DESCRIPTION
-----------
@@ -125,6 +125,14 @@ OPTIONS
`--reedit-message`::
Open an editor to modify the target commit's message.
+`-S[<key-id>]`::
+`--gpg-sign[=<key-id>]`::
+`--no-gpg-sign`::
+ GPG-sign rewritten commits. The _<key-id>_ argument is optional and
+ defaults to the committer identity; if specified, it must be stuck to
+ the option without a space. `--no-gpg-sign` is useful to countermand
+ both `commit.gpgSign` configuration and earlier `--gpg-sign`.
+
`--empty=(drop|keep|abort)`::
Control what happens when a commit becomes empty as a result of the
fixup. This can happen in two situations:
diff --git a/builtin/history.c b/builtin/history.c
index d28c1f08bb..97e0d77013 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -27,13 +27,13 @@
#include "wt-status.h"
#define GIT_HISTORY_DROP_USAGE \
- N_("git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]")
+ N_("git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]")
#define GIT_HISTORY_FIXUP_USAGE \
- N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]")
+ N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)] [--[no-]gpg-sign[=<key-id>]]")
#define GIT_HISTORY_REWORD_USAGE \
- N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]")
+ N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]]")
#define GIT_HISTORY_SPLIT_USAGE \
- N_("git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]")
+ N_("git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--[no-]gpg-sign[=<key-id>]] [--] [<pathspec>...]")
static void change_data_free(void *util, const char *str UNUSED)
{
@@ -105,12 +105,37 @@ enum commit_tree_flags {
COMMIT_TREE_EDIT_MESSAGE = (1 << 0),
};
+static int history_config(const char *var, const char *value,
+ const struct config_context *ctx, void *data)
+{
+ const char **sign_commit = data;
+
+ if (!strcmp(var, "commit.gpgsign")) {
+ *sign_commit = git_config_bool(var, value) ? "" : NULL;
+ return 0;
+ }
+
+ return git_default_config(var, value, ctx, NULL);
+}
+
+#define OPT_HISTORY_GPG_SIGN(v) { \
+ .type = OPTION_STRING, \
+ .short_name = 'S', \
+ .long_name = "gpg-sign", \
+ .value = (v), \
+ .argh = N_("key-id"), \
+ .help = N_("GPG-sign rewritten commits"), \
+ .flags = PARSE_OPT_OPTARG, \
+ .defval = (intptr_t)"", \
+}
+
static int commit_tree_ext(struct repository *repo,
const char *action,
struct commit *commit_with_message,
const struct commit_list *parents,
const struct object_id *old_tree,
const struct object_id *new_tree,
+ const char *sign_commit,
struct commit **out,
enum commit_tree_flags flags)
{
@@ -151,7 +176,7 @@ static int commit_tree_ext(struct repository *repo,
ret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree,
parents, &rewritten_commit_oid, original_author,
- NULL, NULL, original_extra_headers);
+ NULL, sign_commit, original_extra_headers);
if (ret < 0)
goto out;
@@ -167,6 +192,7 @@ static int commit_tree_ext(struct repository *repo,
static int commit_tree_with_edited_message(struct repository *repo,
const char *action,
struct commit *original,
+ const char *sign_commit,
struct commit **out)
{
struct object_id parent_tree_oid;
@@ -188,7 +214,8 @@ static int commit_tree_with_edited_message(struct repository *repo,
}
return commit_tree_ext(repo, action, original, original->parents,
- &parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);
+ &parent_tree_oid, tree_oid, sign_commit, out,
+ COMMIT_TREE_EDIT_MESSAGE);
}
enum ref_action {
@@ -344,12 +371,14 @@ static int compute_pending_ref_updates(struct rev_info *revs,
enum ref_action action,
struct commit *original,
struct commit *rewritten,
+ const char *sign_commit,
enum replay_empty_commit_action empty,
struct replay_result *result)
{
const struct name_decoration *decoration;
struct replay_revisions_options opts = {
.empty = empty,
+ .sign_commit = sign_commit,
};
char hex[GIT_MAX_HEXSZ + 1];
bool detached_head;
@@ -454,13 +483,14 @@ static int handle_reference_updates(struct rev_info *revs,
struct commit *rewritten,
const char *reflog_msg,
int dry_run,
+ const char *sign_commit,
enum replay_empty_commit_action empty)
{
struct replay_result result = { 0 };
int ret;
ret = compute_pending_ref_updates(revs, action, original, rewritten,
- empty, &result);
+ sign_commit, empty, &result);
if (ret)
goto out;
@@ -522,6 +552,7 @@ static int cmd_history_fixup(int argc,
enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;
enum ref_action action = REF_ACTION_DEFAULT;
enum commit_tree_flags flags = 0;
+ const char *sign_commit = NULL;
int dry_run = 0;
struct option options[] = {
OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
@@ -535,6 +566,7 @@ static int cmd_history_fixup(int argc,
OPT_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)",
N_("how to handle commits that become empty"),
PARSE_OPT_NONEG, parse_opt_empty),
+ OPT_HISTORY_GPG_SIGN(&sign_commit),
OPT_END(),
};
struct merge_result merge_result = { 0 };
@@ -546,12 +578,12 @@ static int cmd_history_fixup(int argc,
bool skip_commit = false;
int ret;
+ repo_config(repo, history_config, &sign_commit);
argc = parse_options(argc, argv, prefix, options, usage, 0);
if (argc != 1) {
ret = error(_("command expects a single revision"));
goto out;
}
- repo_config(repo, git_default_config, NULL);
if (action == REF_ACTION_DEFAULT)
action = REF_ACTION_BRANCHES;
@@ -676,7 +708,7 @@ static int cmd_history_fixup(int argc,
if (!skip_commit) {
ret = commit_tree_ext(repo, "fixup", original, original->parents,
&original_tree->object.oid, &merge_result.tree->object.oid,
- &rewritten, flags);
+ sign_commit, &rewritten, flags);
if (ret < 0) {
ret = error(_("failed writing fixed-up commit"));
goto out;
@@ -686,7 +718,7 @@ static int cmd_history_fixup(int argc,
strbuf_addf(&reflog_msg, "fixup: updating %s", argv[0]);
ret = handle_reference_updates(&revs, action, original, rewritten,
- reflog_msg.buf, dry_run, empty);
+ reflog_msg.buf, dry_run, sign_commit, empty);
if (ret < 0) {
ret = error(_("failed replaying descendants"));
goto out;
@@ -711,6 +743,7 @@ static int cmd_history_reword(int argc,
NULL,
};
enum ref_action action = REF_ACTION_DEFAULT;
+ const char *sign_commit = NULL;
int dry_run = 0;
struct option options[] = {
OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
@@ -718,6 +751,7 @@ static int cmd_history_reword(int argc,
PARSE_OPT_NONEG, parse_ref_action),
OPT_BOOL('n', "dry-run", &dry_run,
N_("perform a dry-run without updating any refs")),
+ OPT_HISTORY_GPG_SIGN(&sign_commit),
OPT_END(),
};
struct strbuf reflog_msg = STRBUF_INIT;
@@ -725,12 +759,12 @@ static int cmd_history_reword(int argc,
struct rev_info revs = { 0 };
int ret;
+ repo_config(repo, history_config, &sign_commit);
argc = parse_options(argc, argv, prefix, options, usage, 0);
if (argc != 1) {
ret = error(_("command expects a single revision"));
goto out;
}
- repo_config(repo, git_default_config, NULL);
if (action == REF_ACTION_DEFAULT)
action = REF_ACTION_BRANCHES;
@@ -745,7 +779,8 @@ static int cmd_history_reword(int argc,
if (ret)
goto out;
- ret = commit_tree_with_edited_message(repo, "reworded", original, &rewritten);
+ ret = commit_tree_with_edited_message(repo, "reworded", original,
+ sign_commit, &rewritten);
if (ret < 0) {
ret = error(_("failed writing reworded commit"));
goto out;
@@ -754,7 +789,8 @@ static int cmd_history_reword(int argc,
strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
ret = handle_reference_updates(&revs, action, original, rewritten,
- reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
+ reflog_msg.buf, dry_run, sign_commit,
+ REPLAY_EMPTY_COMMIT_ABORT);
if (ret < 0) {
ret = error(_("failed replaying descendants"));
goto out;
@@ -816,6 +852,7 @@ static int write_ondisk_index(struct repository *repo,
static int split_commit(struct repository *repo,
struct commit *original,
struct pathspec *pathspec,
+ const char *sign_commit,
struct commit **out)
{
struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
@@ -893,7 +930,8 @@ static int split_commit(struct repository *repo,
* that shall be diffed against is the parent of the original commit.
*/
ret = commit_tree_ext(repo, "split-out", original, original->parents, &parent_tree_oid,
- &split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);
+ &split_tree->object.oid, sign_commit, &first_commit,
+ COMMIT_TREE_EDIT_MESSAGE);
if (ret < 0) {
ret = error(_("failed writing first commit"));
goto out;
@@ -910,7 +948,8 @@ static int split_commit(struct repository *repo,
new_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;
ret = commit_tree_ext(repo, "split-out", original, parents, old_tree_oid,
- new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);
+ new_tree_oid, sign_commit, &second_commit,
+ COMMIT_TREE_EDIT_MESSAGE);
if (ret < 0) {
ret = error(_("failed writing second commit"));
goto out;
@@ -938,6 +977,7 @@ static int cmd_history_split(int argc,
NULL,
};
enum ref_action action = REF_ACTION_DEFAULT;
+ const char *sign_commit = NULL;
int dry_run = 0;
struct option options[] = {
OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
@@ -945,6 +985,7 @@ static int cmd_history_split(int argc,
PARSE_OPT_NONEG, parse_ref_action),
OPT_BOOL('n', "dry-run", &dry_run,
N_("perform a dry-run without updating any refs")),
+ OPT_HISTORY_GPG_SIGN(&sign_commit),
OPT_END(),
};
struct commit *original, *rewritten = NULL;
@@ -953,12 +994,12 @@ static int cmd_history_split(int argc,
struct rev_info revs = { 0 };
int ret;
+ repo_config(repo, history_config, &sign_commit);
argc = parse_options(argc, argv, prefix, options, usage, 0);
if (argc < 1) {
ret = error(_("command expects a committish"));
goto out;
}
- repo_config(repo, git_default_config, NULL);
if (action == REF_ACTION_DEFAULT)
action = REF_ACTION_BRANCHES;
@@ -984,14 +1025,15 @@ static int cmd_history_split(int argc,
goto out;
}
- ret = split_commit(repo, original, &pathspec, &rewritten);
+ ret = split_commit(repo, original, &pathspec, sign_commit, &rewritten);
if (ret < 0)
goto out;
strbuf_addf(&reflog_msg, "split: updating %s", argv[0]);
ret = handle_reference_updates(&revs, action, original, rewritten,
- reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
+ reflog_msg.buf, dry_run, sign_commit,
+ REPLAY_EMPTY_COMMIT_ABORT);
if (ret < 0) {
ret = error(_("failed replaying descendants"));
goto out;
@@ -1081,6 +1123,7 @@ static int cmd_history_drop(int argc,
};
enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;
enum ref_action action = REF_ACTION_DEFAULT;
+ const char *sign_commit = NULL;
int dry_run = 0;
struct option options[] = {
OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
@@ -1091,6 +1134,7 @@ static int cmd_history_drop(int argc,
OPT_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)",
N_("how to handle descendants that become empty"),
PARSE_OPT_NONEG, parse_opt_empty),
+ OPT_HISTORY_GPG_SIGN(&sign_commit),
OPT_END(),
};
struct strbuf reflog_msg = STRBUF_INIT;
@@ -1101,12 +1145,12 @@ static int cmd_history_drop(int argc,
bool head_moves = false;
int ret;
+ repo_config(repo, history_config, &sign_commit);
argc = parse_options(argc, argv, prefix, options, usage, 0);
if (argc != 1) {
ret = error(_("command expects a single revision"));
goto out;
}
- repo_config(repo, git_default_config, NULL);
if (action == REF_ACTION_DEFAULT)
action = REF_ACTION_BRANCHES;
@@ -1134,7 +1178,7 @@ static int cmd_history_drop(int argc,
rewritten = original->parents->item;
ret = compute_pending_ref_updates(&revs, action, original, rewritten,
- empty, &result);
+ sign_commit, empty, &result);
if (ret) {
ret = error(_("failed replaying descendants"));
goto out;
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
index de7b357685..6dbe2143d3 100755
--- a/t/t3451-history-reword.sh
+++ b/t/t3451-history-reword.sh
@@ -4,6 +4,7 @@ test_description='tests for git-history reword subcommand'
. ./test-lib.sh
. "$TEST_DIRECTORY/lib-log-graph.sh"
+. "$TEST_DIRECTORY/lib-gpg.sh"
reword_with_message () {
cat >message &&
@@ -26,6 +27,37 @@ expect_log () {
test_cmp expect actual
}
+test_reword_gpg_sign () {
+ must_fail= will=will
+ if test "x$1" = "x!"
+ then
+ must_fail=test_must_fail
+ will="will not"
+ shift
+ fi
+ conf=$1
+ shift
+
+ test_expect_success GPG "reword $* with commit.gpgsign=$conf $will sign rewritten history" "
+ test_when_finished 'rm -rf repo' &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git config commit.gpgsign $conf &&
+ reword_with_message $* HEAD~ <<-EOF &&
+ second reworded
+ EOF
+
+ $must_fail git verify-commit HEAD~ &&
+ $must_fail git verify-commit HEAD
+ )
+ "
+}
+
test_expect_success 'can reword tip of a branch' '
test_when_finished "rm -rf repo" &&
git init repo &&
@@ -77,6 +109,37 @@ test_expect_success 'can reword commit in the middle' '
)
'
+test_reword_gpg_sign ! false
+test_reword_gpg_sign true
+test_reword_gpg_sign false --gpg-sign
+test_reword_gpg_sign ! true --no-gpg-sign
+test_reword_gpg_sign ! true --gpg-sign --no-gpg-sign
+test_reword_gpg_sign false --no-gpg-sign --gpg-sign
+
+test_expect_success GPG 'reword uses an explicit signing key for rewritten history' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ reword_with_message -SB7227189 HEAD~ <<-EOF &&
+ second reworded
+ EOF
+
+ git verify-commit HEAD~ &&
+ git verify-commit HEAD &&
+ git log -2 --format=%GK >actual &&
+ cat >expect <<-\EOF &&
+ 65A0EEA02E30CAD7
+ 65A0EEA02E30CAD7
+ EOF
+ test_cmp expect actual
+ )
+'
+
test_expect_success 'can reword commit in the middle even on detached head' '
test_when_finished "rm -rf repo" &&
git init repo &&
diff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh
index 8ed0cebb50..e96f492cc6 100755
--- a/t/t3452-history-split.sh
+++ b/t/t3452-history-split.sh
@@ -4,6 +4,7 @@ test_description='tests for git-history split subcommand'
. ./test-lib.sh
. "$TEST_DIRECTORY/lib-log-graph.sh"
+. "$TEST_DIRECTORY/lib-gpg.sh"
# The fake editor takes multiple arguments, each of which represents a commit
# message. Subsequent invocations of the editor will then yield those messages
@@ -36,6 +37,42 @@ expect_tree_entries () {
test_cmp expect actual
}
+test_split_gpg_sign () {
+ must_fail= will=will
+ if test "x$1" = "x!"
+ then
+ must_fail=test_must_fail
+ will="will not"
+ shift
+ fi
+ conf=$1
+ shift
+
+ test_expect_success GPG "split $* with commit.gpgsign=$conf $will sign rewritten history" "
+ test_when_finished 'rm -rf repo' &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit initial &&
+ touch bar foo &&
+ git add . &&
+ git commit -m split-me &&
+ test_commit tip &&
+
+ git config commit.gpgsign $conf &&
+ set_fake_editor 'first' 'second' &&
+ git history split $* HEAD~ <<-EOF &&
+ y
+ n
+ EOF
+
+ $must_fail git verify-commit HEAD~2 &&
+ $must_fail git verify-commit HEAD~ &&
+ $must_fail git verify-commit HEAD
+ )
+ "
+}
+
test_expect_success 'refuses to work with merge commits' '
test_when_finished "rm -rf repo" &&
git init repo &&
@@ -141,6 +178,13 @@ test_expect_success 'can split up tip commit' '
)
'
+test_split_gpg_sign ! false
+test_split_gpg_sign true
+test_split_gpg_sign false --gpg-sign
+test_split_gpg_sign ! true --no-gpg-sign
+test_split_gpg_sign ! true --gpg-sign --no-gpg-sign
+test_split_gpg_sign false --no-gpg-sign --gpg-sign
+
test_expect_success 'can split up root commit' '
test_when_finished "rm -rf repo" &&
git init repo &&
diff --git a/t/t3453-history-fixup.sh b/t/t3453-history-fixup.sh
index 868298e248..cd20a23115 100755
--- a/t/t3453-history-fixup.sh
+++ b/t/t3453-history-fixup.sh
@@ -3,6 +3,7 @@
test_description='tests for git-history fixup subcommand'
. ./test-lib.sh
+. "$TEST_DIRECTORY/lib-gpg.sh"
fixup_with_message () {
cat >message &&
@@ -21,6 +22,37 @@ expect_changes () {
test_cmp expect actual
}
+test_fixup_gpg_sign () {
+ must_fail= will=will
+ if test "x$1" = "x!"
+ then
+ must_fail=test_must_fail
+ will="will not"
+ shift
+ fi
+ conf=$1
+ shift
+
+ test_expect_success GPG "fixup $* with commit.gpgsign=$conf $will sign rewritten history" "
+ test_when_finished 'rm -rf repo' &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git config commit.gpgsign $conf &&
+ echo fix >>second.t &&
+ git add second.t &&
+ git history fixup $* HEAD~ &&
+
+ $must_fail git verify-commit HEAD~ &&
+ $must_fail git verify-commit HEAD
+ )
+ "
+}
+
test_expect_success 'errors on missing commit argument' '
test_when_finished "rm -rf repo" &&
git init repo &&
@@ -229,6 +261,13 @@ test_expect_success 'preserves commit message and authorship' '
)
'
+test_fixup_gpg_sign ! false
+test_fixup_gpg_sign true
+test_fixup_gpg_sign false --gpg-sign
+test_fixup_gpg_sign ! true --no-gpg-sign
+test_fixup_gpg_sign ! true --gpg-sign --no-gpg-sign
+test_fixup_gpg_sign false --no-gpg-sign --gpg-sign
+
test_expect_success 'updates all descendant branches by default' '
test_when_finished "rm -rf repo" &&
git init repo --initial-branch=main &&
diff --git a/t/t3454-history-drop.sh b/t/t3454-history-drop.sh
index 68a86d1e37..5b21078a7e 100755
--- a/t/t3454-history-drop.sh
+++ b/t/t3454-history-drop.sh
@@ -4,6 +4,7 @@ test_description='tests for git-history drop subcommand'
. ./test-lib.sh
. "$TEST_DIRECTORY/lib-log-graph.sh"
+. "$TEST_DIRECTORY/lib-gpg.sh"
expect_graph () {
cat >expect &&
@@ -16,6 +17,34 @@ expect_log () {
test_cmp expect actual
}
+test_drop_gpg_sign () {
+ must_fail= will=will
+ if test "x$1" = "x!"
+ then
+ must_fail=test_must_fail
+ will="will not"
+ shift
+ fi
+ conf=$1
+ shift
+
+ test_expect_success GPG "drop $* with commit.gpgsign=$conf $will sign replayed descendants" "
+ test_when_finished 'rm -rf repo' &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git config commit.gpgsign $conf &&
+ git history drop $* HEAD~ &&
+
+ $must_fail git verify-commit HEAD
+ )
+ "
+}
+
test_expect_success 'errors on missing commit argument' '
test_when_finished "rm -rf repo" &&
git init repo &&
@@ -88,6 +117,27 @@ test_expect_success 'drops a commit in the middle and replays descendants' '
)
'
+test_drop_gpg_sign ! false
+test_drop_gpg_sign true
+test_drop_gpg_sign false --gpg-sign
+test_drop_gpg_sign ! true --no-gpg-sign
+test_drop_gpg_sign ! true --gpg-sign --no-gpg-sign
+test_drop_gpg_sign false --no-gpg-sign --gpg-sign
+
+test_expect_success GPG 'drop has no commit to sign when dropping the tip' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+
+ git history drop --gpg-sign HEAD &&
+
+ test_must_fail git verify-commit HEAD
+ )
+'
+
test_expect_success 'drops the HEAD commit' '
test_when_finished "rm -rf repo" &&
git init repo &&
--
2.55.0
^ permalink raw reply related [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-07-17 14:52 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-03 14:50 [PATCH 0/3] history: sign rewritten commits Souma
2026-07-03 14:50 ` [PATCH 1/3] builtin/history: " Souma
2026-07-16 10:18 ` Patrick Steinhardt
2026-07-03 14:50 ` [PATCH 2/3] doc: document history signing options Souma
2026-07-16 10:18 ` Patrick Steinhardt
2026-07-03 14:50 ` [PATCH 3/3] t345x: cover signed history rewrites Souma
2026-07-17 14:51 ` [PATCH v2 0/2] history: support signing rewritten commits Souma
2026-07-17 14:51 ` [PATCH v2 1/2] replay: allow callers to sign commits Souma
2026-07-17 14:51 ` [PATCH v2 2/2] builtin/history: sign rewritten commits Souma
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.