aboutsummaryrefslogtreecommitdiff
path: root/t
diff options
context:
space:
mode:
authorPatrick Steinhardt <ps@pks.im>2026-01-13 10:54:39 +0100
committerJunio C Hamano <gitster@pobox.com>2026-01-13 05:41:17 -0800
commitd205234cb05a5e330c0f7f5b3ea764533a74d69e (patch)
tree8977722181bf9b8eb485d94ff7257a8e03f866fa /t
parenta675183d483cb106c076395936cd9e602ae94404 (diff)
downloadgit-d205234cb05a5e330c0f7f5b3ea764533a74d69e.tar.xz
builtin/history: implement "reword" subcommand
Implement a new "reword" subcommand for git-history(1). This subcommand is similar to the user performing an interactive rebase with a single commit changed to use the "reword" instruction. The "reword" subcommand is built on top of the replay subsystem instead of the sequencer. This leads to some major differences compared to git-rebase(1): - We do not check out the commit that is to be reworded and instead perform the operation in-memory. This has the obvious benefit of being significantly faster compared to git-rebase(1), but even more importantly it allows the user to rewrite history even if there are local changes in the working tree or in the index. - We do not execute any hooks, even though we leave some room for changing this in the future. - By default, all local branches that contain the commit will be rewritten. This especially helps with workflows that use stacked branches. Helped-by: Elijah Newren <newren@gmail.com> Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Diffstat (limited to 't')
-rw-r--r--t/meson.build1
-rwxr-xr-xt/t3450-history.sh6
-rwxr-xr-xt/t3451-history-reword.sh391
3 files changed, 395 insertions, 3 deletions
diff --git a/t/meson.build b/t/meson.build
index 73006b095a..c9f92450dc 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -388,6 +388,7 @@ integration_tests = [
't3437-rebase-fixup-options.sh',
't3438-rebase-broken-files.sh',
't3450-history.sh',
+ 't3451-history-reword.sh',
't3500-cherry.sh',
't3501-revert-cherry-pick.sh',
't3502-cherry-pick-merge.sh',
diff --git a/t/t3450-history.sh b/t/t3450-history.sh
index 417c343d43..f513463b92 100755
--- a/t/t3450-history.sh
+++ b/t/t3450-history.sh
@@ -5,13 +5,13 @@ test_description='tests for git-history command'
. ./test-lib.sh
test_expect_success 'does nothing without any arguments' '
- git history >out 2>&1 &&
- test_must_be_empty out
+ test_must_fail git history 2>err &&
+ test_grep "need a subcommand" err
'
test_expect_success 'raises an error with unknown argument' '
test_must_fail git history garbage 2>err &&
- test_grep "unrecognized argument: garbage" err
+ test_grep "unknown subcommand: .garbage." err
'
test_done
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
new file mode 100755
index 0000000000..3594421b68
--- /dev/null
+++ b/t/t3451-history-reword.sh
@@ -0,0 +1,391 @@
+#!/bin/sh
+
+test_description='tests for git-history reword subcommand'
+
+. ./test-lib.sh
+. "$TEST_DIRECTORY/lib-log-graph.sh"
+
+reword_with_message () {
+ cat >message &&
+ write_script fake-editor.sh <<-\EOF &&
+ cp message "$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+ git history reword "$@" &&
+ rm fake-editor.sh message
+}
+
+expect_graph () {
+ cat >expect &&
+ lib_test_cmp_graph --graph --format=%s "$@"
+}
+
+expect_log () {
+ git log --format="%s" "$@" >actual &&
+ cat >expect &&
+ test_cmp expect actual
+}
+
+test_expect_success 'can reword tip of a branch' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ reword_with_message HEAD <<-EOF &&
+ third reworded
+ EOF
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ expect_log <<-\EOF &&
+ third reworded
+ second
+ first
+ EOF
+
+ git reflog >reflog &&
+ test_grep "reword: updating HEAD" reflog
+ )
+'
+
+test_expect_success 'can reword commit in the middle' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ git symbolic-ref HEAD >expect &&
+ reword_with_message HEAD~ <<-EOF &&
+ second reworded
+ EOF
+ git symbolic-ref HEAD >actual &&
+ test_cmp expect actual &&
+
+ expect_log <<-\EOF
+ third
+ second reworded
+ first
+ EOF
+ )
+'
+
+test_expect_success 'can reword commit in the middle even on detached head' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third_on_main &&
+ git checkout --detach HEAD^ &&
+ test_commit third_on_head &&
+
+ reword_with_message HEAD~ <<-EOF &&
+ second reworded
+ EOF
+
+ expect_graph HEAD --branches <<-\EOF
+ * third_on_head
+ | * third_on_main
+ |/
+ * second reworded
+ * first
+ EOF
+ )
+'
+
+test_expect_success 'can reword the detached head' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ git checkout --detach HEAD &&
+ test_commit third &&
+
+ reword_with_message HEAD <<-EOF &&
+ third reworded
+ EOF
+
+ expect_log <<-\EOF
+ third reworded
+ second
+ first
+ EOF
+ )
+'
+
+test_expect_success 'can reword root commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+ reword_with_message HEAD~2 <<-EOF &&
+ first reworded
+ EOF
+
+ expect_log <<-\EOF
+ third
+ second
+ first reworded
+ EOF
+ )
+'
+
+test_expect_success 'can reword in a bare repo' '
+ test_when_finished "rm -rf repo repo.git" &&
+ git init repo &&
+ test_commit -C repo first &&
+ git clone --bare repo repo.git &&
+ (
+ cd repo.git &&
+ reword_with_message HEAD <<-EOF &&
+ reworded
+ EOF
+
+ expect_log <<-\EOF
+ reworded
+ EOF
+ )
+'
+
+test_expect_success 'can reword a commit on a different branch' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch theirs &&
+ test_commit ours &&
+ git switch theirs &&
+ test_commit theirs &&
+
+ git rev-parse ours >ours-before &&
+ reword_with_message theirs <<-EOF &&
+ Reworded theirs
+ EOF
+ git rev-parse ours >ours-after &&
+ test_cmp ours-before ours-after &&
+
+ expect_graph --branches <<-\EOF
+ * Reworded theirs
+ | * ours
+ |/
+ * base
+ EOF
+ )
+'
+
+test_expect_success 'can reword a merge commit' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+ git switch - &&
+ git merge theirs &&
+
+ # It is not possible to replay merge commits embedded in the
+ # history (yet).
+ test_must_fail git history reword HEAD~ 2>err &&
+ test_grep "replaying merge commits is not supported yet" err &&
+
+ # But it is possible to reword a merge commit directly.
+ reword_with_message HEAD <<-EOF &&
+ Reworded merge commit
+ EOF
+ expect_graph <<-\EOF
+ * Reworded merge commit
+ |\
+ | * theirs
+ * | ours
+ |/
+ * base
+ EOF
+ )
+'
+
+test_expect_success '--ref-action=print prints ref updates without modifying repo' '
+ test_when_finished "rm -rf repo" &&
+ git init repo --initial-branch=main &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit ours &&
+ git switch branch &&
+ test_commit theirs &&
+
+ git refs list >refs-expect &&
+ reword_with_message --ref-action=print base >updates <<-\EOF &&
+ reworded commit
+ EOF
+ git refs list >refs-actual &&
+ test_cmp refs-expect refs-actual &&
+
+ test_grep "update refs/heads/branch" updates &&
+ test_grep "update refs/heads/main" updates &&
+ git update-ref --stdin <updates &&
+ expect_log --branches <<-\EOF
+ theirs
+ ours
+ reworded commit
+ EOF
+ )
+'
+
+test_expect_success '--ref-action=head updates only HEAD' '
+ test_when_finished "rm -rf repo" &&
+ git init repo --initial-branch=main &&
+ (
+ cd repo &&
+ test_commit base &&
+ git branch branch &&
+ test_commit theirs &&
+ git switch branch &&
+ test_commit ours &&
+
+ # When told to update HEAD, only, the command will refuse to
+ # rewrite commits that are not an ancestor of HEAD.
+ test_must_fail git history reword --ref-action=head theirs 2>err &&
+ test_grep "rewritten commit must be an ancestor of HEAD" err &&
+
+ reword_with_message --ref-action=head base >updates <<-\EOF &&
+ reworded base
+ EOF
+ expect_log HEAD <<-\EOF &&
+ ours
+ reworded base
+ EOF
+ expect_log main <<-\EOF
+ theirs
+ base
+ EOF
+ )
+'
+
+test_expect_success 'editor shows proper status' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+
+ write_script fake-editor.sh <<-\EOF &&
+ cp "$1" . &&
+ printf "\namend a comment\n" >>"$1"
+ EOF
+ test_set_editor "$(pwd)"/fake-editor.sh &&
+ git history reword HEAD &&
+
+ cat >expect <<-EOF &&
+ first
+
+ # Please enter the commit message for the reworded changes. Lines starting
+ # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
+ # Changes to be committed:
+ # new file: first.t
+ #
+ EOF
+ test_cmp expect COMMIT_EDITMSG &&
+
+ test_commit_message HEAD <<-\EOF
+ first
+
+ amend a comment
+ EOF
+ )
+'
+
+# For now, git-history(1) does not yet execute any hooks. This is subject to
+# change in the future, and if it does this test here is expected to start
+# failing. In other words, this test is not an endorsement of the current
+# status quo.
+test_expect_success 'hooks are not executed for rewritten commits' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+ test_commit second &&
+ test_commit third &&
+
+ ORIG_PATH="$(pwd)" &&
+ export ORIG_PATH &&
+ for hook in prepare-commit-msg pre-commit post-commit post-rewrite commit-msg
+ do
+ write_script .git/hooks/$hook <<-\EOF || exit 1
+ touch "$ORIG_PATH/hooks.log
+ EOF
+ done &&
+
+ reword_with_message HEAD~ <<-EOF &&
+ second reworded
+ EOF
+
+ cat >expect <<-EOF &&
+ third
+ second reworded
+ first
+ EOF
+ git log --format=%s >actual &&
+ test_cmp expect actual &&
+
+ test_path_is_missing hooks.log
+ )
+'
+
+test_expect_success 'aborts with empty commit message' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ test_commit first &&
+
+ ! reword_with_message HEAD 2>err </dev/null &&
+ test_grep "Aborting commit due to empty commit message." err
+ )
+'
+
+test_expect_success 'retains changes in the worktree and index' '
+ test_when_finished "rm -rf repo" &&
+ git init repo &&
+ (
+ cd repo &&
+ touch a b &&
+ git add . &&
+ git commit -m "initial commit" &&
+ echo foo >a &&
+ echo bar >b &&
+ git add b &&
+ reword_with_message HEAD <<-EOF &&
+ message
+ EOF
+ cat >expect <<-\EOF &&
+ M a
+ M b
+ ?? actual
+ ?? expect
+ EOF
+ git status --porcelain >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_done