Rebase (git)
Rebasing in git is a way to clean up your commit history and change the history of one branch to be after the changes of another. Most commonly, you rebase a feature branch on the end of the master/main branch.
The Golden Rule
If your branch is being worked on by anybody else but you, do not rebase! Because rebase is destructive, you will not be able to reconcile branches with git, leaving you to do the work manually. 💀⚰️
From Linus Torvald[10]:
People can (and probably should) rebase their private trees (their own work). That's a cleanup. But never other peoples code. That's a "destroy history"
Another Good Rule
ALWAYS make a backup of the branch you are trying to rebase with. If you screw it up, the commits are gone. Make a copy (git commit -b branch-name-bak
), run your test, ensure it did the thing you wanted it to do, then hold on to your backup in case you were wrong.
Merge vs. Rebase
Merging and rebasing both achieve the same goals, but in different ways. Merging non-destructively maintains the history of the current branch when merging in other branches, with the cost of adding a new merge commit. Rebasing destructively changes the history of the current branch when rebasing on to other branches, but leaves no new commits.
Simple Rebase
To perform a simple rebase and put the commits of the current branch on to the tip of another branch, you can use the following command:
git rebase {other-branch}
For instance, if I were on a feature
branch and I wanted to rebase on top of the main
branch, I would use git rebase main
. This will re-write the history destructively and prompt you if there are any conflicts to resolve before proceeding.
Interactive Rebase
To rebase one branch onto the nearest common ancestor of another, use the following command:
git rebase -i `git merge-base {other-branch} HEAD`
For instance, if I was on a feature
branch and wanted to interactively rebase over all commits that occurred since it branched off of main
, it would be git rebase -i `git merge-base main HEAD`
.
Note that when the commits are listed, they are listed oldest to newest, unlike the git log which is newest to oldest. This is very important!
Alias
In your ~/.gitconfig
file, you can add this in the [alias]
section to invoke the above command using git rbm {other-branch}
:
rbm = "! f() { git rebase -i `git merge-base ${1} HEAD`; }; f"
Interactive Rebase Commands
I only have used these so far, and they do the job for what I need.
Command(s) | Effect |
---|---|
p / pick |
Keep the commit as it currently is |
r / reword |
Keep the commit and edit the commit message |
s / squash |
Combine this commit with the previous commit, and edit commit message for newly squashed commit |
f / fixup |
Fold this commit into the previous commit, using the previous commit's message |
e / edit |
Stop at this commit and give user control until rebase --continue [9] |
Revert back to unresolved state/conflicts [19]
If you have made changes to a file that had merge conflicts and want to restart the conflict resolution, use the following command to bring them back.
$ git checkout -m FILE
Rebasing Onto Squashed Commits[15-18] or Different Base Branches
First, MAKE A BACKUP!
Different Base Branch
Let's say you build a feature (feature
) on top of your main branch (main
) when you should have developed it on the dev branch (dev
).
feature G-H-I
/
dev / D-E-F
/ /
main A-B-C-'
This can be solved fairly easily by using git rebase --onto
. By noting the commit just before the first commit made on your new feature
branch (e.g. C
, as it is the commit right before G
on feature
), you can replay each of the commits from C
forward (e.g. G
, H
, I
) on top of the dev
branch.
# git checkout starting-branch
git checkout feature
# git rebase --onto target-branch-or-commit commit-to-replay-from
# In this, the commit hash for `C` is `c0ffeebb`
git rebase --onto dev c0ffeebb
Doing this results in this branch structure:
feature G-H-I
/
dev D-E-F
/
main A-B-C
Squashed Commits
Lets say that you had three branches: main
, feature
, and ft-addition
. ft-addition
built upon the work of feature
.
ft-addition G-H-I
/
feature D-E-F
/
main A-B-C
We finish with feature
and squash it before merging it into main (squashed commit being S
.
ft-addition D-E-F-G-H-I
/
main A-B-C-S
We now have conflicting commit histories. The content of S
is all of the commits D
, E
, F
. If we git rebase main
while checked out in the ft-addition
branch, we would have to deal with conflicts on every commit as it tried to replay D
, E
, and F
on top of S
. This would mean lots of unnecessary merge conflict resolution.
To fix this, we can use the --onto
flag on git rebase
. The syntax is as follows:
For instance, with our example, we would want to replay commits G
, H
, and I
but not D
, E
, or F
. So we would use:
git checkout ft-addition
git rebase --onto main F
This replays all commits that are children of commit F
onto the commit found at main
.
This would result in this structure:
ft-addition G-H-I
/
main A-B-C-S
We change the parent of commit G
from F
to S
.
Have I Rebased and Merged This Branch?[13]
One problem with a rebase and merge or squash and rebase before merge is that the standard git branch -d branch-name
will reject as it is not recognized as merged in to the target branch. A couple ways to check:
- Use
git log --oneline --cherry target-branch...starting-branch
to see which commits are present in both branches. - If commit messages are maybe not true to the original, or a squash and rebase has occurred, you can use
git checkout target-branch~0; git merge starting-branch
. This will put you on a detached head of the target branch and try to merge in the starting branch's content. If this commit is merged in already, it should sayAlready up to date.
.
You can put all of option 2 into a function in your shell as an alias.
grbdiff () {
local TARGET=$(git rev-parse --abbrev-ref HEAD)
local STARTING="$1"
git checkout "${TARGET}~0" 2>/dev/null
git merge "${STARTING}" 2>/dev/null
local DIFF=$(gd --stat)
if [[ -z "$DIFF" ]]
then
echo -e "Branch '${TARGET}' contains contents of '${STARTING}'. Nothing to merge."
else
echo -e "Branch '${TARGET}' does not contain contents of '${STARTING}':\n${DIFF}"
fi
git checkout "${TARGET}" 2>/dev/null
}
It can be executed by checking out the target branch and typing grbdiff starting-branch
. If there are changes, it will print the diff.
Package Lock Conflicts[14]
If you end up with package-lock.json
conflicts in a rebase, follow these instructions:
- This is given that you are at a state in the rebase where all other previous or current conflicts have been managed and all that is left is the package lock.
- Run
npm install --package-lock-only
. - Stage
package-lock.json
in the current commit. - Run
git rebase continue
and commit.
This should render you the up to date package-lock.json
file without having to do all the merge conflicts manually.
References
- https://www.youtube.com/watch?v=7Mh259hfxJg
- https://www.youtube.com/watch?v=u5ywa435MbI
- https://www.atlassian.com/git/tutorials/merging-vs-rebasing
- https://linuxhint.com/git-rebase-tutorial/
- https://git-scm.com/docs/git-rebase
- https://stackoverflow.com/questions/363908/how-do-i-use-git-rebase-i-to-rebase-all-changes-in-a-branch
- https://betterprogramming.pub/git-workflow-etiquette-f22d96b8b0b8#3336
- https://demisx.github.io/git/rebase/2015/07/02/git-rebase-keep-my-branch-changes.html
- https://stackoverflow.com/questions/6217156/break-a-previous-commit-into-multiple-commits
- http://blog.nerdbank.net/2020/01/should-i-merge-or-rebase-in-git.html
- https://blog.carbonfive.com/always-squash-and-rebase-your-git-commits/
- Visual differences between merge, rebase, squash/merge: https://stackoverflow.com/a/43551395/14857724
- https://stackoverflow.com/a/34337939/14857724
- https://marcelofernandes.dev/blog/solving-package-lock-json-conflicts/
- https://tech.bakkenbaeck.com/post/Rebasing_Onto_A_Squashed_Commit
- https://scribe.rip/swiftblade/how-git-rebase-onto-works-71ff00e3f88c
- https://womanonrails.com/git-rebase-onto
- https://git-scm.com/book/en/v2/Git-Branching-Rebasing#rbdiag_e
- https://stackoverflow.com/a/14409744/14857724
Incoming Links
Last modified: 202401040446