In Git Merge and the Multiverse I looked at merging branches and how sometimes this results in a fast-forward merge and sometimes in the creation of a merge commit. Now let’s look at Git rebase (a.k.a. the power to rewrite history).
If you haven’t read Git Merge and the Multiverse yet, I strongly suggest you start there first and then return to this article.
Progress in Both Universes (no conflict)
Revisiting a scenario from Git Merge and the Multiverse, our main
universe branched off into a second universe (feature-a
). In feature-a
, I placed flowers on the coffee table, while in main
I placed a tablecloth on the kitchen table.
(main) (feature-a)
Add tablecloth Add flowers to coffee table
Add kitchen.md Add kitchen.md
When we originally addressed this situation we created a merge commit, a commit that stitches the two separate universes together. Looking at the parents of this merge commit, we see two separate histories (one where flowers are added to the coffee table and one were a tablecloth is added to the kitchen table).
While stitching together this two universes with a merge commit is a perfectly legitimate technique, it doesn’t create the “cleanest” Git history.
Rewriting History
We have an alternative to creating this merge commit, we can instead modify the history of our feature-a
branch. As numerous time travel movies have pointed out, messing with time and modifying history can cause significant problems.
Only rewrite history on branches you have full control over, where full control means either the branch only exists on your local machine (not on a Git hosting platform like GitHub) or the branch does exist on a Git hosting platform but you’ve made it clear to others that the history on this branch may change (I’m a fan of starting a branch name with my initials, to make it clear this is my branch and I’ll rewrite history if I want to (e.g. sf/feature-a
)).
How does Rebase Work
When we rebase feature-a
with main
, the following happens:
- the
feature-a
timeline is rewound back to the last common commit withmain
(i.e. rewinds to beforeAdd flowers to coffee table
in this case) - does a fast-forward merge of the
main
branch timeline intofeature-a
(nowmain
andfeature-a
are identical) - replays the time we previously rewound on
feature-a
(i.e. reapplyAdd flowers to coffee table
)
We end up with the following
(main) (feature-a)
Add flowers to coffee table
Add tablecloth Add tablecloth
Add kitchen.md Add kitchen.md
Why Rebase
After rebasing feature-a
with main
, the feature-a
branch is up to date with main
meaning that if we merged feature-a
into main
it would be a fast-forward merge (we like fast-forward merges).
Technical Details of Rebase without Conflict
Two rebase feature-a
with main
, we checkout feature-a
git checkout feature-a
and then run the rebase command, telling Git what branch we want to use
git rebase main
The following is the output of git log --oneline --graph
after the rebase.
* 12b853b (feature-a) Add flowers to coffee table
* 032193f (main) Add tablecloth
* ea72b65 Add kitchen.md
Notice:
- When we reapply the time we rewound (“Add flowers to coffee table”), the commit gets a new commit hash (
12b853b
). - We are now in a position that we could do a fast-forward merge of
feature-a
intomain
, sincemain
is an ancestor offeature-a
Git repo (before the rebase) available at salcode/git-merge-multiverse-scenario-2
Progress in Both Universes that Overlap
Revisiting our scenario from Git Merge and the Multiverse, our main
branch and feature-b
branch have both moved forward in time but in this case the changes conflict. Flowers were placed on the kitchen table in feature-b
and in main
a tablecloth was placed on the kitchen table.
(main) (feature-b)
Add tablecloth Add flowers to kitchen table
Add kitchen.md Add kitchen.md
In this case, we rewind time on feature-b
(removing the flowers from the kitchen table), merge main
into this rewound feature-b
, and re-apply Add flowers to the kitchen table
.
Originally, when we did Add flowers to the kitchen table
there was no tablecloth on the table. When re-applying Add flowers to the kitchen table
in the rebase, the table does have a table cloth – Git can’t handle this. Git doesn’t know how to handle this.
Auto-merging kitchen.md
CONFLICT (content): Merge conflict in kitchen.md
error: could not apply 9d59323... Add flowers to kitchen table
When we open kitchen.md
we see the conflict
<<<<<<< HEAD
## Kitchen table with table cloth
=======
## Kitchen table withh flowers on top
>>>>>>> 9d59323 (Add flowers to kitchen table)
Using our knowledge and understanding of the situation we resolve this merge conflict (and fix the typo)
## Kitchen table with tablecloth on top with flowers on top
Technical Details of Rebase with Conflict
These are the commands you can run to rebase these branches
git checkout feature-b
git rebase main
at this point, you have a merge conflict as described above. Open kitchen.md
and resolve the merge conflict, then run
git add kitchen.md
git rebase --continue
Now feature-b
has been brought up to date with main
, the two branches now match except feature-b
has additional work on top (so feature-b
can now be fast-forward merged into main
).
Git repo (before the merge) available at salcode/git-merge-multiverse-scenario-3
Git Push Now Fails
When we break the rules of time and rewrite history, by default Git tries to prevent us from messing up the timeline for others. For this reason git push
fails with a message like
error: failed to push some refs
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: ‘git pull …’) before pushing again.
hint: See the ‘Note about fast-forwards’ in ‘git push –help’ for details.
However, we can tell Git that we know what we’re doing and to make this update even though it may seem like a bad idea. The --force-with-lease
flag lets this work.
git push --force-with-lease
(Some might suggest using --force
instead of --force-with-lease
, however in my experience --force-with-lease
is always better. See Never use git push force).
To Rebase or Not to Rebase
The opinions on this topic vary from “never rebase” to “always rebase. My opinion falls in between these two.
When I’m working on my own branch and I haven’t pushed it to the remote Git repo yet, I definitely rebase my branch.
When someone else creates a branch and I load it in my local environment, I definitely do NOT rebase their branch.
If I’ve named the branch starting with my initials (e.g. sf/feature-a
) and I haven’t asked anyone to review the branch yet, I’ll rebase.
If someone has pulled down my branch to their local computer to review a pull request, I don’t rebase (even if the branch name starts with my initials).
Related Articles
- Git Rebase with Blocks (article & video)
- Resolving Git Rebase Merge Conflicts
- See gitgifs.com for block visualizations
Leave a Reply