rebase -i: fix counting of fixups after rebase --skip

When the sequencer processes a chain of "fixup" and "squash" commands
it keeps a list of the commands that have been executed. If there are
conflicts, then the list is saved when the rebase stops for the user to
resolve them. When the rebase resumes, the list is loaded and is used
to initialize the count of how many "fixup" and "squash" commands have
been processed; if a command has been skipped with "git rebase --skip",
then the last command needs to be popped off the end of the list.

To count the number of commands, commit_staged_changes() uses the
number of newlines in the file plus one. This is due to the slightly
unusual way the list is constructed - instead of appending a newline
when a command is added, a newline is inserted before the command
if the current count is greater than zero. Therefore, when we pop a
skipped command off the list, we should also remove the newline that
precedes it. Otherwise, when a new command is added, a blank line
will be left before it, which will contribute to the fixup count the
next time the file is read. Unfortunately, the preceding newline is
not removed, leading to an incorrect count. Fix this by removing the
newline that appears before the skipped command.

In addition to fixing the code that removes a skipped command from the
list, the code that reads the list is fixed to skip blank lines. We
have had reports of users starting a rebase with one version of
git and continuing it with another. Often this happens because the
version of git bundled with an IDE or TUI differs from the one used
at the command line. By fixing both the reading and writing ends of
the problem we ensure the count is correct when an older version of
git reads the fixup file written by a newer version and vice versa.

Triggering the incorrect count requires the user to skip two "fixup" or
"squash" commands before the final command in the chain. An existing
test is extended to prevent future regressions. The consequence of
miscounting is not serious: we just print the wrong count in the
header of the commit message template.

Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Phillip Wood 2026-07-17 17:06:36 +01:00 committed by Junio C Hamano
parent d35c5399e3
commit c0d2ac475e
2 changed files with 42 additions and 5 deletions

View File

@ -3281,7 +3281,13 @@ static int read_populate_opts(struct replay_opts *opts)
const char *p = ctx->current_fixups.buf;
ctx->current_fixup_count = 1;
while ((p = strchr(p, '\n'))) {
ctx->current_fixup_count++;
/*
* Older versions of git accidentally
* inserted blank lines when a fixup
* was skipped.
*/
if (p[1] != '\n')
ctx->current_fixup_count++;
p++;
}
}
@ -5354,6 +5360,9 @@ static int commit_staged_changes(struct repository *r,
BUG("Incorrect current_fixups:\n%s", p);
while (len && p[len - 1] != '\n')
len--;
/* Remove trailing newline */
if (len)
len--;
strbuf_setlen(&ctx->current_fixups, len);
if (write_message(p, len, rebase_path_current_fixups(),
0) < 0) {

View File

@ -134,6 +134,7 @@ test_expect_success '--skip after failed fixup cleans commit message' '
EOF

: skip and continue &&
test_config commit.status false &&
echo "cp \"\$1\" .git/copy.txt" | write_script copy-editor.sh &&
(test_set_editor "$PWD/copy-editor.sh" && git rebase --skip) &&

@ -145,7 +146,8 @@ test_expect_success '--skip after failed fixup cleans commit message' '

: now, let us ensure that "squash" is handled correctly &&
git reset --hard wants-fixup-3 &&
test_must_fail env FAKE_LINES="1 squash 2 squash 1 squash 3 squash 1" \
test_must_fail env \
FAKE_LINES="1 squash 2 squash 1 squash 3 squash 1 squash 4 squash 1" \
git rebase -i HEAD~4 &&

: the second squash failed, but there are two more in the chain &&
@ -171,6 +173,32 @@ test_expect_success '--skip after failed fixup cleans commit message' '
fixup 2
EOF

(test_set_editor "$PWD/copy-editor.sh" &&
test_must_fail git rebase --skip) &&
: not the final squash, no need to edit the commit message &&
test_path_is_missing .git/copy.txt &&

: The first, third and fifth squashes succeeded, therefore: &&
cat >expect <<-\EOF &&
# This is a combination of 4 commits.
# This is the 1st commit message:

wants-fixup

# This is the commit message #2:

fixup 1

# This is the commit message #3:

fixup 2

# This is the commit message #4:

fixup 3
EOF
test_commit_message HEAD expect &&

(test_set_editor "$PWD/copy-editor.sh" && git rebase --skip) &&
test_commit_message HEAD <<-\EOF &&
wants-fixup
@ -178,12 +206,12 @@ test_expect_success '--skip after failed fixup cleans commit message' '
fixup 1

fixup 2

fixup 3
EOF

: Final squash failed, but there was still a squash &&
head -n1 .git/copy.txt >first-line &&
test_grep "# This is a combination of 3 commits" first-line &&
test_grep "# This is the commit message #3:" .git/copy.txt
test_cmp expect .git/copy.txt
'

test_expect_success 'setup rerere database' '