blob: 868298e248fe494d71bd899e33336f6ae90820eb [file] [edit]
#!/bin/sh
test_description='tests for git-history fixup subcommand'
. ./test-lib.sh
fixup_with_message () {
cat >message &&
write_script fake-editor.sh <<-\EOF &&
cp message "$1"
EOF
test_set_editor "$(pwd)"/fake-editor.sh &&
git history fixup --reedit-message "$@" &&
rm fake-editor.sh message
}
expect_changes () {
git log --format="%s" --numstat "$@" >actual.raw &&
sed '/^$/d' <actual.raw >actual &&
cat >expect &&
test_cmp expect actual
}
test_expect_success 'errors on missing commit argument' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
test_must_fail git history fixup 2>err &&
test_grep "command expects a single revision" err
)
'
test_expect_success 'errors on too many arguments' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
test_must_fail git history fixup HEAD HEAD 2>err &&
test_grep "command expects a single revision" err
)
'
test_expect_success 'errors on unknown revision' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
test_must_fail git history fixup does-not-exist 2>err &&
test_grep "commit cannot be found: does-not-exist" err
)
'
test_expect_success 'errors when nothing is staged' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
test_must_fail git history fixup HEAD 2>err &&
test_grep "nothing to fixup: no staged changes" err
)
'
test_expect_success 'errors in a bare repository' '
test_when_finished "rm -rf repo repo.git" &&
git init repo &&
test_commit -C repo initial &&
git clone --bare repo repo.git &&
test_must_fail git -C repo.git history fixup HEAD 2>err &&
test_grep "cannot run fixup in a bare repository" err
'
test_expect_success 'errors with invalid --empty= value' '
test_when_finished "rm -rf repo" &&
git init repo &&
test_must_fail git -C repo history fixup --empty=bogus HEAD 2>err &&
test_grep "unrecognized.*--empty.*bogus" err
'
test_expect_success 'can fixup the tip commit' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
echo content >file.txt &&
git add file.txt &&
git commit -m "add file" &&
echo fix >>file.txt &&
git add file.txt &&
expect_changes <<-\EOF &&
add file
1 0 file.txt
initial
1 0 initial.t
EOF
git symbolic-ref HEAD >branch-expect &&
git history fixup HEAD &&
git symbolic-ref HEAD >branch-actual &&
test_cmp branch-expect branch-actual &&
expect_changes <<-\EOF &&
add file
2 0 file.txt
initial
1 0 initial.t
EOF
# Verify the fix is in the tip commit tree
git show HEAD:file.txt >actual &&
printf "content\nfix\n" >expect &&
test_cmp expect actual &&
git reflog >reflog &&
test_grep "fixup: updating HEAD" reflog
)
'
test_expect_success 'can fixup a commit in the middle of history' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit first &&
echo content >file.txt &&
git add file.txt &&
git commit -m "add file" &&
test_commit third &&
echo fix >>file.txt &&
git add file.txt &&
expect_changes <<-\EOF &&
third
1 0 third.t
add file
1 0 file.txt
first
1 0 first.t
EOF
git history fixup HEAD~ &&
expect_changes <<-\EOF &&
third
1 0 third.t
add file
2 0 file.txt
first
1 0 first.t
EOF
# Verify the fix landed in the "add file" commit.
git show HEAD~:file.txt >actual &&
printf "content\nfix\n" >expect &&
test_cmp expect actual &&
# And verify that the replayed commit also has the change.
git show HEAD:file.txt >actual &&
printf "content\nfix\n" >expect &&
test_cmp expect actual
)
'
test_expect_success 'can fixup root commit' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
echo initial >root.txt &&
git add root.txt &&
git commit -m "root" &&
test_commit second &&
expect_changes <<-\EOF &&
second
1 0 second.t
root
1 0 root.txt
EOF
echo fix >>root.txt &&
git add root.txt &&
git history fixup HEAD~ &&
expect_changes <<-\EOF &&
second
1 0 second.t
root
2 0 root.txt
EOF
git show HEAD~:root.txt >actual &&
printf "initial\nfix\n" >expect &&
test_cmp expect actual
)
'
test_expect_success 'preserves commit message and authorship' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
echo content >file.txt &&
git add file.txt &&
git commit --author="Original <original@example.com>" -m "original message" &&
echo fix >>file.txt &&
git add file.txt &&
git history fixup HEAD &&
# Message preserved
git log -1 --format="%s" >actual &&
echo "original message" >expect &&
test_cmp expect actual &&
# Authorship preserved
git log -1 --format="%an <%ae>" >actual &&
echo "Original <original@example.com>" >expect &&
test_cmp expect actual
)
'
test_expect_success 'updates all descendant branches by default' '
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 switch main &&
echo fix >fix.txt &&
git add fix.txt &&
git history fixup base &&
expect_changes --branches <<-\EOF &&
theirs
1 0 theirs.t
ours
1 0 ours.t
base
1 0 base.t
1 0 fix.txt
EOF
# Both branches should have the fix in the base
git show main~:fix.txt >actual &&
echo fix >expect &&
test_cmp expect actual &&
git show branch~:fix.txt >actual &&
test_cmp expect actual
)
'
test_expect_success 'can fixup 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 &&
# Stage a change while on "theirs"
echo fix >fix.txt &&
git add fix.txt &&
# Ensure that "ours" does not change, as it does not contain
# the commit in question.
git rev-parse ours >ours-before &&
git history fixup theirs &&
git rev-parse ours >ours-after &&
test_cmp ours-before ours-after &&
git show HEAD:fix.txt >actual &&
echo fix >expect &&
test_cmp expect actual
)
'
test_expect_success '--dry-run 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 main-tip &&
git switch branch &&
test_commit branch-tip &&
git switch main &&
echo fix >fix.txt &&
git add fix.txt &&
git refs list >refs-before &&
git history fixup --dry-run base >updates &&
git refs list >refs-after &&
test_cmp refs-before refs-after &&
test_grep "update refs/heads/main" updates &&
test_grep "update refs/heads/branch" updates &&
expect_changes --branches <<-\EOF &&
branch-tip
1 0 branch-tip.t
main-tip
1 0 main-tip.t
base
1 0 base.t
EOF
git update-ref --stdin <updates &&
expect_changes --branches <<-\EOF
branch-tip
1 0 branch-tip.t
main-tip
1 0 main-tip.t
base
1 0 base.t
1 0 fix.txt
EOF
)
'
test_expect_success '--update-refs=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 main-tip &&
git switch branch &&
test_commit branch-tip &&
echo fix >fix.txt &&
git add fix.txt &&
# Only HEAD (branch) should be updated
git history fixup --update-refs=head base &&
# The main branch should be unaffected.
expect_changes main <<-\EOF &&
main-tip
1 0 main-tip.t
base
1 0 base.t
EOF
# But the currently checked out branch should be modified.
expect_changes branch <<-\EOF
branch-tip
1 0 branch-tip.t
base
1 0 base.t
1 0 fix.txt
EOF
)
'
test_expect_success '--update-refs=head refuses to rewrite commits not in HEAD ancestry' '
test_when_finished "rm -rf repo" &&
git init repo --initial-branch=main &&
(
cd repo &&
test_commit base &&
git branch other &&
test_commit main-tip &&
git switch other &&
test_commit other-tip &&
echo fix >fix.txt &&
git add fix.txt &&
test_must_fail git history fixup --update-refs=head main-tip 2>err &&
test_grep "rewritten commit must be an ancestor of HEAD" err
)
'
test_expect_success 'aborts when fixup would produce conflicts' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
echo "line one" >file.txt &&
git add file.txt &&
git commit -m "first" &&
echo "line two" >file.txt &&
git add file.txt &&
git commit -m "second" &&
echo "conflicting change" >file.txt &&
git add file.txt &&
git refs list >refs-before &&
test_must_fail git history fixup HEAD~ 2>err &&
test_grep "fixup would produce conflicts" err &&
git refs list >refs-after &&
test_cmp refs-before refs-after
)
'
test_expect_success '--reedit-message opens editor for the commit message' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
echo content >file.txt &&
git add file.txt &&
git commit -m "add file" &&
echo fix >>file.txt &&
git add file.txt &&
fixup_with_message HEAD <<-\EOF &&
add file with fix
EOF
expect_changes --branches <<-\EOF
add file with fix
2 0 file.txt
initial
1 0 initial.t
EOF
)
'
test_expect_success 'retains unstaged working tree changes after fixup' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
touch a b &&
git add . &&
git commit -m "initial commit" &&
echo staged >a &&
echo unstaged >b &&
git add a &&
git history fixup HEAD &&
# b is still modified in the worktree but not staged
cat >expect <<-\EOF &&
M b
EOF
git status --porcelain --untracked-files=no >actual &&
test_cmp expect actual
)
'
test_expect_success 'index is clean after fixup when target is HEAD' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
echo fix >fix.txt &&
git add fix.txt &&
git history fixup HEAD &&
git status --porcelain --untracked-files=no >actual &&
test_must_be_empty actual
)
'
test_expect_success 'index is unchanged on conflict' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
echo base >file.txt &&
git add file.txt &&
git commit -m base &&
echo change >file.txt &&
git add file.txt &&
git commit -m change &&
echo conflict >file.txt &&
git add file.txt &&
git diff --cached >index-before &&
test_must_fail git history fixup HEAD~ &&
git diff --cached >index-after &&
test_cmp index-before index-after
)
'
test_expect_success '--empty=drop removes target commit and replays descendants onto its parent' '
test_when_finished "rm -rf repo" &&
git init repo --initial-branch=main &&
(
cd repo &&
test_commit first &&
test_commit second &&
test_commit third &&
git rm second.t &&
git history fixup --empty=drop HEAD~ &&
expect_changes <<-\EOF &&
third
1 0 third.t
first
1 0 first.t
EOF
test_must_fail git show HEAD:second.t
)
'
test_expect_success '--empty=drop errors out when dropping the root commit' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit first &&
test_commit second &&
git rm first.t &&
test_must_fail git history fixup --empty=drop HEAD~ 2>err &&
test_grep "cannot drop root commit" err
)
'
test_expect_success '--empty=drop can drop the HEAD commit' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit first &&
test_commit second &&
git rm second.t &&
git history fixup --empty=drop HEAD &&
expect_changes <<-\EOF
first
1 0 first.t
EOF
)
'
test_expect_success '--empty=drop drops empty replayed commits' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
touch base remove-me &&
git add . &&
git commit -m "base" &&
git rm remove-me &&
git commit -m "remove" &&
touch reintroduce remove-me &&
git add . &&
git commit -m "reintroduce" &&
git rm remove-me &&
git history fixup --empty=drop HEAD~2 &&
expect_changes <<-\EOF
reintroduce
0 0 reintroduce
0 0 remove-me
base
0 0 base
EOF
)
'
test_expect_success '--empty=keep keeps commit when fixup target becomes empty' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit first &&
test_commit second &&
test_commit third &&
git rm second.t &&
git history fixup --empty=keep HEAD~ &&
expect_changes <<-\EOF
third
1 0 third.t
second
first
1 0 first.t
EOF
)
'
test_expect_success '--empty=keep keeps commit when replayed commit becomes empty' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
touch base remove-me &&
git add . &&
git commit -m "base" &&
git rm remove-me &&
git commit -m "remove" &&
touch reintroduce remove-me &&
git add . &&
git commit -m "reintroduce" &&
git rm remove-me &&
git history fixup --empty=keep HEAD~2 &&
expect_changes <<-\EOF
reintroduce
0 0 reintroduce
0 0 remove-me
remove
base
0 0 base
EOF
)
'
test_expect_success '--empty=abort errors out when fixup target becomes empty' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit first &&
test_commit second &&
git rm first.t &&
test_must_fail git history fixup --empty=abort HEAD~ 2>err &&
test_grep "fixup makes commit.*empty" err
)
'
test_expect_success '--empty=abort errors out when a descendant becomes empty during replay' '
test_when_finished "rm -rf repo" &&
git init repo --initial-branch=main &&
(
cd repo &&
touch base remove-me &&
git add . &&
git commit -m "base" &&
git rm remove-me &&
git commit -m "remove" &&
touch reintroduce remove-me &&
git add . &&
git commit -m "reintroduce" &&
git rm remove-me &&
test_must_fail git history fixup --empty=abort HEAD~2 2>err &&
test_grep "became empty after replay" err
)
'
test_done