Search code examples
gitgit-mergerebase

Git rebase commit replays vs merge commits: a concrete example


I have a question about how rebasing works in git, in part because whenever I ask other devs questions about it I get vague, abstract, high level "architect-y speak" that doesn't make a whole lot of sense to me.

It sounds as if rebasing "replays" commits, one after another (so sequentially) from the source branch over the changes in my working branch, is this the case? So if I have a feature branch, say, feature/xyz-123 that was cut from develop originally, and then I rebase from origin/develop, then it replays all the commits made to develop since I branched off of it. Furthermore, it does so, one develop commit at a time, until all the changes have been "replayed" into my feature branch, yes?

If anything I have said above is incorrect or misled, please begin by correcting me! But assuming I'm more or less correct, I'm not seeing how this is any different than merging in changes from develop by doing a git merge develop. Don't both methods result with all the latest changes from develop making their way into feature/xyz-123?

I'm sure this is not the case but I'm just not seeing the forest through the trees here. If someone could give a concrete example (with perhaps some mock commits and git command line invocations) I might be able to understand the difference in how rebase works versus a merge. Thanks in advance!


Solution

  • RomainValeri's answer covers a lot of this, but let me add a few more items:

    • "Replaying" a commit is a matter of running git cherry-pick or doing something equivalent. Some versions of git rebase literally do run git cherry-pick and if you want to think about how rebase works, that's the concrete piece to consider. (If you want to get into all the side details, the other principal way to "copy" a commit is to use git format-patch and git am: this goes faster because it doesn't handle renames properly. So the cherry-pick method is normally better. It was not the default until relatively recently, though, except for interactive rebases and any rebases that used the interactive machinery.)

    • Merge commits literally cannot be cherry-picked, so when using git rebase --rebase-merges, Git can't do that. Instead, Git will re-perform the merges. That is, Git figures out, at the start of the rebase operation, which commits are ordinary (single-parent) commits that can be cherry-picked, and which ones are merges with two or more parents. It then lays out, in the interactive rebase script—this particular kind of rebase always uses the interactive machinery—the special extra commands that interactive rebase needs in order to run git merge again. You can see these commands by adding --interactive to your rebase command. Be careful if you edit them: they have to be run in the right order for everything to work.

    • If you have merge commits that git rebase would need to "replay", and you don't use --rebase-merges, git rebase simply drops these merge commits entirely. The result is ... often not good. 😀 But if you did a merge that you did not mean to do, and want to do a rebase instead, it actually is good: this strips out the final merge.

    The TL;DR version of this is: both are complicated. Using git merge often makes one (1) new merge commit, that combines work. Using git rebase copies, as if by cherry-pick, many commits, and abandons the old (and lousy?) commits for the new-and-improved copies.

    When using either git cherry-pick or git merge, merge conflicts can occur. Git 2.33 is likely to have git rerere enabled by default, so that any previous resolution for some particular merge conflict will be picked up automatically and re-used. Currently you must enable this manually. The way rerere works is a bit complicated; it's worth reading through this blog entry.