Empty commits removed after interactive rebase, even though --keep-empty is used

Viewed 6260

I have some trouble using the --keep-empty option of git rebase, and I'm not sure whether I'm misunderstanding what this option does, or there's a bug.

Here is a minimal example:

Setup

  1. Create a new Git repository and an initial, unrelated commit.

    $ git init
    $ echo something >base.txt
    $ git add base.txt
    $ git commit -m 'some base commit to not run into the root corner case'
    
  2. Create a new commit which adds two new files.

    $ echo A >a.txt; echo B >b.txt
    $ git add a.txt b.txt
    $ git commit -m 'add A and B'
    
  3. Modify one of the files.

    $ echo A1 >a.txt
    $ git add a.txt
    $ git commit -m 'change A'
    
  4. Modify the other file.

    $ echo B1 >b.txt
    $ git add b.txt
    $ git commit -m 'change B'
    

Rebase

$ git checkout -b rebased master
$ git rebase --keep-empty -i :/base

… choosing to edit the commit where A and B are added, and changing it so that only B is added (in a real scenario the reason might be that A is confidential):

$ git rm a.txt
$ git commit --amend
$ git rebase --continue

Naturally, the next commit where A is modified now gives a conflict:

error: could not apply 182aaa1... change A

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
Could not apply 182aaa1701ad100fc02a5d5500cacebdd317a24b... change A

… choosing to not add the modified version of a.txt:

$ git mergetool
Merging:
a.txt

Deleted merge conflict for 'a.txt':
  {local}: deleted
  {remote}: modified file
Use (m)odified or (d)eleted file, or (a)bort? d

The commit where A was modified is now empty:

$ git diff --cached
# nothing

… and finishing the rebase:

$ git rebase --continue
Successfully rebased and updated refs/heads/rebased.

Question

So now I have two versions of my history, with the difference that there is no trace of A in one of them. However, because I chose the --keep-empty option, I still expect an empty commit to exist in rebased, which would show me that A would have been modified, had it been there.

But apparently, this is not the case:

$ git log --oneline master
f893569 change B
182aaa1 change A
3340b71 add A and B
38cb5da some base commit to not run into the root corner case

$ git log --oneline rebased
73a2c05 change B
55f502b add A and B
38cb5da some base commit to not run into the root corner case

Is this not what --keep-empty is supposed to do, or does it not work correctly?


Related: Rebase on the root and keep empty commits is a very similar question, but it involves the --root corner case which I explicitly avoided here. And it has no answer, only some comments which suggest that what I'm showing here should work. Another difference is that in the other question the commit is empty in the first place, while here it only becomes empty after resolving a conflict.

3 Answers

However, because I chose the --keep-empty option, I still expect an empty commit to exist in rebased, which would show me that A would have been modified, had it been there.

But apparently, this is not the case:

Double-check that with Git 2.18 (Q2 2018), considering "git rebase --keep-empty" still removed an empty commit if the other side contained an empty commit (due to the "does an equivalent patch exist already?" check), which has been corrected.

See commit 3d94616, commit 76ea235, commit bb2ac4f (20 Mar 2018) by Phillip Wood (phillipwood).
(Merged by Junio C Hamano -- gitster -- in commit d892bee, 25 Apr 2018)

rebase -i --keep-empty: don't prune empty commits

If there are empty commits on the left hand side of $upstream...HEAD then the empty commits on the right hand side that we want to keep are pruned by --cherry-pick.
Fix this by using --cherry-mark instead of --cherry-pick and keeping the commits that are empty or are not marked as cherry-picks.

And:

rebase --keep-empty: always use interactive rebase

rebase --merge accepts --keep-empty but just ignores it, by using an implicit interactive rebase the user still gets the rename detection of a merge based rebase but with with --keep-empty support.

If rebase --keep-empty without --interactive or --merge stops for the user to resolve merge conflicts then 'git rebase --continue' will fail. This is because it uses a different code path that does not create $git_dir/rebase-apply.
As rebase --keep-empty was implemented using cherry-pick it has never supported the am options and now that interactive rebases support --signoff there is no loss of functionality by using an implicit interactive rebase.


Note: this is part of a larger feature added to git rebase in Git 2.18:
See "What exactly does Git's “rebase --preserve-merges” do (and why?)".
With git --rebase-merges (which will ultimately replace the old git --preserve-merges), you now can rebase a whole topology of commit graph elsewhere.


With Git 2.27 (Q2 2020), "git rebase" (again) learns to honor "--no-keep-empty", which lets the user to discard commits that are empty from the beginning (as opposed to the ones that become empty because of rebasing).

The interactive rebase also marks commits that are empty in the todo.

See commit 50ed761, commit b9cbd29, commit 1b5735f (11 Apr 2020) by Elijah Newren (newren).
(Merged by Junio C Hamano -- gitster -- in commit c7d8f69, 22 Apr 2020)

rebase: reinstate --no-keep-empty

Reported-by: Bryan Turner
Reported-by: Sami Boukortt
Signed-off-by: Elijah Newren

Commit d48e5e21da ("rebase (interactive-backend): make --keep-empty the default", 2020-02-15, Git v2.26.0-rc0 -- merge listed in batch #8) turned --keep-empty (for keeping commits which start empty) into the default.

The logic underpinning that commit was:

  1. 'git commit' errors out on the creation of empty commits without an override flag
  2. Once someone determines that the override is worthwhile, it's annoying and/or harmful to required them to take extra steps in order to keep such commits around (and to repeat such steps with every rebase).

While the logic on which the decision was made is sound, the result was a bit of an overcorrection.

Instead of jumping to having --keep-empty being the default, it jumped to making --keep-empty the only available behavior.

There was a simple workaround, though, which was thought to be good enough at the time.

People could still drop commits which started empty the same way the could drop any commits: by firing up an interactive rebase and picking out the commits they didn't want from the list.

However, there are cases where external tools might create enough empty commits that picking all of them out is painful.

As such, having a flag to automatically remove start-empty commits may be beneficial.

Provide users a way to drop commits which start empty using a flag that existed for years: --no-keep-empty.

Interpret --keep-empty as countermanding any previous --no-keep-empty, but otherwise leaving --keep-empty as the default.

This might lead to some slight weirdness since commands like:

git rebase --empty=drop --keep-empty
git rebase --empty=keep --no-keep-empty

look really weird despite making perfect sense (the first will drop commits which become empty, but keep commits that started empty; the second will keep commits which become empty, but drop commits which started empty).

However, --no-keep-empty was named years ago and we are predominantly keeping it for backward compatibility; also we suspect it will only be used rarely since folks already have a simple way to drop commits they don't want with an interactive rebase.

i also met this issue.

  • 3328dbe - add hello world test code for jpa — Minghui Ma (HEAD) - (14 minutes ago)
  • 3fd2d95 - init empty commit

when i use cmd " git rebase -i --root --keep-empty" there was no commit "3fd2d95 - init empty commit"

so i force insert one line "pick 3fd2d95 init empty commit" at the first line.

Related