Merge branch 'ss/history-sign-rewrites' into seen

The git history command and its subcommands have been updated to
honor the commit.gpgSign configuration variable and accept the
--gpg-sign and --no-gpg-sign command-line options, ensuring both
rewritten commits and their replayed descendants are
cryptographically signed by the committer.

* ss/history-sign-rewrites:
  history: sign rewritten commits
  replay: allow callers to sign commits
seen
Junio C Hamano 2026-10-12 09:26:14 -07:00
commit f09e2f0cc4
10 changed files with 358 additions and 34 deletions

View File

@ -8,11 +8,11 @@ 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 squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] <revision-range>
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>...]
git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] [--[no-]gpg-sign[=<key-id>]] <revision-range>

DESCRIPTION
-----------
@ -180,6 +180,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:

View File

@ -30,15 +30,15 @@
#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>...]")
#define GIT_HISTORY_SQUASH_USAGE \
N_("git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] <revision-range>")
N_("git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] [--[no-]gpg-sign[=<key-id>]] <revision-range>")

static void change_data_free(void *util, const char *str UNUSED)
{
@ -110,6 +110,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, 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,
@ -117,6 +141,7 @@ static int commit_tree_ext(struct repository *repo,
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)
{
@ -160,7 +185,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;

@ -196,6 +221,7 @@ static int first_parent_tree_oid(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;
@ -207,7 +233,8 @@ static int commit_tree_with_edited_message(struct repository *repo,
return -1;

return commit_tree_ext(repo, action, original, NULL, 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 {
@ -363,12 +390,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;
@ -473,13 +502,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;

@ -533,6 +563,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)",
@ -546,6 +577,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 };
@ -557,12 +589,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;
@ -687,7 +719,7 @@ static int cmd_history_fixup(int argc,
if (!skip_commit) {
ret = commit_tree_ext(repo, "fixup", original, NULL, 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;
@ -697,7 +729,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;
@ -722,6 +754,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)",
@ -729,6 +762,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;
@ -736,12 +770,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;
@ -756,7 +790,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;
@ -765,7 +800,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;
@ -831,6 +867,7 @@ out:
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;
@ -900,8 +937,10 @@ static int split_commit(struct repository *repo,
* The first commit is constructed from the split-out tree. The base
* that shall be diffed against is the parent of the original commit.
*/
ret = commit_tree_ext(repo, "split-out", original, NULL, original->parents, &parent_tree_oid,
&split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);
ret = commit_tree_ext(repo, "split-out", original, NULL, original->parents,
&parent_tree_oid, &split_tree->object.oid, sign_commit,
&first_commit,
COMMIT_TREE_EDIT_MESSAGE);
if (ret < 0) {
ret = error(_("failed writing first commit"));
goto out;
@ -918,7 +957,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, NULL, 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;
@ -946,6 +986,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)",
@ -953,6 +994,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;
@ -961,12 +1003,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;
@ -992,14 +1034,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;
@ -1579,6 +1622,7 @@ static int cmd_history_squash(int argc,
NULL,
};
enum ref_action action = REF_ACTION_DEFAULT;
const char *sign_commit = NULL;
int dry_run = 0;
int edit = 1;
struct option options[] = {
@ -1589,6 +1633,7 @@ static int cmd_history_squash(int argc,
N_("perform a dry-run without updating any refs")),
OPT_BOOL('e', "edit", &edit,
N_("edit the commit message")),
OPT_HISTORY_GPG_SIGN(&sign_commit),
OPT_END(),
};
struct strbuf reflog_msg = STRBUF_INIT;
@ -1598,13 +1643,13 @@ static int cmd_history_squash(int argc,
struct rev_info revs = { 0 };
int ret;

repo_config(repo, history_config, &sign_commit);
argc = parse_options(argc, argv, prefix, options, usage,
PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_ARGV0);
if (argc < 2) {
ret = error(_("command expects a revision range"));
goto out;
}
repo_config(repo, git_default_config, NULL);

if (action == REF_ACTION_DEFAULT)
action = REF_ACTION_BRANCHES;
@ -1629,7 +1674,7 @@ static int cmd_history_squash(int argc,

ret = commit_tree_ext(repo, "squash", oldest, message_template,
oldest->parents, base_tree_oid, tip_tree_oid,
&rewritten,
sign_commit, &rewritten,
edit ? COMMIT_TREE_EDIT_MESSAGE : 0);
if (ret < 0) {
ret = error(_("failed writing squashed commit"));
@ -1638,6 +1683,7 @@ static int cmd_history_squash(int argc,

ret = handle_reference_updates(&revs, action, tip, rewritten,
reflog_msg.buf, dry_run,
sign_commit,
REPLAY_EMPTY_COMMIT_ABORT);
if (ret < 0) {
ret = error(_("failed replaying descendants"));
@ -1728,6 +1774,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)",
@ -1738,6 +1785,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;
@ -1748,12 +1796,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;
@ -1781,7 +1829,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;

View File

@ -85,13 +85,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();
@ -288,7 +288,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 tree *pickme_tree, *base_tree, *replayed_base_tree;

@ -361,7 +362,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)
@ -481,7 +483,8 @@ int replay_revisions(struct rev_info *revs,

last_commit = pick_regular_commit(revs->repo, commit, base,
&merge_opt, &result,
mode, opts->empty);
mode, opts->empty,
opts->sign_commit);
}

if (!last_commit)

View File

@ -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.

View File

@ -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 &&

View File

@ -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 &&

View File

@ -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 &&

View File

@ -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 &&

View File

@ -3,6 +3,7 @@
test_description='tests for git-history squash subcommand'

. ./test-lib.sh
. "$TEST_DIRECTORY/lib-gpg.sh"

stage_file () {
printf "%s\n" "$1" >file &&
@ -40,6 +41,66 @@ check_commit_author () {
test_cmp expect actual
}

test_squash_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 "squash $* 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 &&
test_commit fourth &&

git config commit.gpgsign $conf &&
git history squash --no-edit $* HEAD~3..HEAD~1 &&

$must_fail git verify-commit HEAD~ &&
$must_fail git verify-commit HEAD
)
"
}

test_squash_gpg_sign ! false
test_squash_gpg_sign true
test_squash_gpg_sign false --gpg-sign
test_squash_gpg_sign ! true --no-gpg-sign
test_squash_gpg_sign ! true --gpg-sign --no-gpg-sign
test_squash_gpg_sign false --no-gpg-sign --gpg-sign

test_expect_success GPG 'squash 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 &&
test_commit fourth &&

git history squash --no-edit -SB7227189 HEAD~3..HEAD~1 &&

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 'setup linear history touching two files' '
test_commit base file a start &&
GIT_AUTHOR_NAME=One GIT_AUTHOR_EMAIL=one@example.com \

View File

@ -3236,6 +3236,8 @@ test_expect_success 'git history subcommand options' '
test_completion "git history split main --" <<-\EOF &&
--update-refs=Z
--dry-run Z
--gpg-sign Z
--no-... Z
--no-dry-run Z
EOF
test_completion "git history fixup --upd" "--update-refs=" &&