Advanced Rebasing in Git
Advanced Rebasing in Git 관련
Now that you understand the basics of rebase, it is time to consider more advanced cases, where additional switches and arguments to the rebase
command will come in handy.
In the previous example, when you only said rebase
(without additional switches), Git replayed all the commits from the common ancestor to the tip of the current branch.
But rebase is a super-power, it's an almighty command capable of…well, rewriting history. And it can come in handy if you want to modify history to make it your own.
Undo the last merge by making main
point to "Commit 4" again:
git reset -–hard <ORIGINAL_COMMIT 4>
And undo the rebasing by using:
git checkout paul_branch
git reset -–hard <ORIGINAL_COMMIT 9>
Notice that you got to exactly the same history you used to have:
Again, to be clear, "Commit 9" doesn't just disappear when it's not reachable from the current HEAD
. Rather, it's still stored in the object database. And as you used git reset
now to change HEAD
to point to this commit, you were able to retrieve it, and also its parent commits since they are also stored in the database. Pretty cool, huh? 😎
OK, quickly view the changes that Paul introduced:
git show HEAD
Keep going backwards in the commit graph:
git show HEAD~
And one commit further:
git show HEAD~2
So, these changes are nice, but perhaps Paul doesn't want this kind of history. Rather, he wants it to seem as if he introduced the changes in "Commit 7" and "Commit 8" as a single commit.
For that, you can use an interactive rebase. To do that, we add the -i
(or --interactive
) switch to the rebase
command:
git rebase -i <SHA_OF_COMMIT_4>
Or, since main
is pointing to "Commit 4", we can simply run:
git rebase -i main
By running this command, you tell Git to use a new base, "Commit 4". So you are asking Git to go back to all commits that were introduced after "Commit 4" and that are reachable from the current HEAD
, and replay those commits.
For every commit that is replayed, Git asks us what we'd like to do with it:
In this context it's useful to think of a commit as a patch. That is, "Commit 7" as in "the patch that "Commit 7" introduced on top of its parent".
One option is to use pick
. This is the default behavior, which tells Git to replay the changes introduced in this commit. In this case, if you just leave it as is – and pick
all commits – you will get the same history, and Git won't even create new commit objects.
Another option is squash
. A squashed commit will have its contents "folded" into the contents of the commit preceding it. So in our case, Paul would like to squash "Commit 8" into "Commit 7":
As you can see, git rebase -i
provides additional options, but we won't go into all of them in this post. If you allow the rebase to run, you will get prompted to select a commit message for the newly created commit (that is, the one that introduced the changes of both "Commit 7" and "Commit 8"):
And look at the history:
Exactly as we wanted! We have on paul_branch
"Commit 9" (of course, it's a different object than the original "Commit 9"). This points to "Commits 7+8", which is a single commit introducing the changes of both the original "Commit 7" and the original "Commit 8". This commit's parent is "Commit 4", where main
is pointing to. You have john_branch
.
Oh wow, isn't that cool? 😎
git rebase
grants you unlimited control over the shape of any branch. You can use it to reorder commits, or to remove incorrect changes, or modify a change in retrospect. Alternatively, you could perhaps move the base of your branch onto another commit, any commit that you wish.
How to Use the --onto
Switch of git rebase
Let's consider one more example. Get to main
again:
git checkout main
And delete the pointers to paul_branch
and john_branch
so you don't see them in the commit graph anymore:
git branch -D paul_branch
git branch -D john_branch
And now branch from main
to a new branch:
git checkout -b new_branch
Now, add a few changes here and commit them:
nano code.py
git add code.py
git commit -m "Commit 10"
Get back to main
:
git checkout main
And introduce another change:
Time to stage and commit these changes:
git add code.py
git commit -m "Commit 11"
And yet another change:
Commit this change as well:
git add code.py
git commit -m "Commit 12"
Oh wait, now I realize that I wanted you to make the changes introduced in "Commit 11" as a part of the new_branch
. Ugh. What can you do? 🤔
Consider the history:
What I want is, instead of having "Commit 10" reside only on the main
branch, I want it to be on both the main
branch as well as the new_branch
. Visually, I would want to move it down the graph here:
Can you see where I am going? 😇
Well, as we understand, rebase allows us to basically replay the changes introduced in new_branch
, those introduced in "Commit 10", as if they had been originally conducted on "Commit 11", rather than "Commit 4".
To do that, you can use other arguments of git rebase
. You'd tell Git that you want to take all the history introduced between the common ancestor of main
and new_branch
, which is "Commit 4", and have the new base for that history be "Commit 11". To do that, use:
git rebase -–onto <SHA_OF_COMMIT_11> main new_branch
And look at our beautiful history! 😍
Let's consider another case.
Say I started working on a branch, and by mistake I started working from feature_branch_1
, rather than from main
.
So to emulate this, create feature_branch_1
:
git checkout main
git checkout -b feature_branch_1
And erase new_branch
so you don't see it in the graph anymore:
git branch -D new_branch
Create a simple Python file called 1.py
:
Stage and commit this file:
git add 1.py
git commit -m "Commit 13"
Now branched out (by mistake) from feature_branch_1
:
git checkout -b feature_branch_2
And create another file, 2.py
:
Stage and commit this file as well:
git add 2.py
git commit -m "Commit 14"
And introduce some more code to 2.py
:
Stage and commit these changes too:
git add 2.py
git commit -m "Commit 15"
So far you should have this history:
Get back to feature_branch_1
and edit 1.py
:
git checkout feature_branch_1
Now stage and commit:
git add 1.py
git commit -m "Commit 16"
Your history should look like this:
Say now you realize, you've made a mistake. You actually wanted feature_branch_2
to be born from the main
branch, rather than from feature_branch_1
.
How can you achieve that? 🤔
Try to think about it given the history graph and what you've learned about the --onto
flag for the rebase
command.
Well, you want to "replace" the parent of your first commit on feature_branch_2
, which is "Commit 14", to be on top of main
branch, in this case, "Commit 12", rather than the beginning of feature_branch_1
, in this case, "Commit 13". So again, you will be creating a new base, this time for the first commit on feature_branch_2
.
How would you do that?
First, switch to feature_branch_2
:
git checkout feature_branch_2
And now you can use:
git rebase -–onto main <SHA_OF_COMMIT_13>
As a result, you have feature_branch_2
based on main
rather than feature_branch_1
:
The syntax is of the command is:
git rebase --onto <NEW_PARENT> <OLD_PARENT>