loading...

git deep dive part 6: Rebase

kenakamu profile image Kenichiro Nakamura ・5 min read

In the previous article, I explain how to merge branches. In this article, I explain how git rebase works.

What is rebase

Rebase is restart branching from certain point. For example, If I create dev branch from master right now, it looks like below.

Alt Text

Then I create commits in dev branch.

Alt Text

But someone introduced change in master.

Alt Text

Now I want to rebase from the latest commit of the master.

Alt Text

The reason I put ??????? to commit id is that we already know the id should be different now as commit file contains different data.

Check the current log and prepare for test

At the moment, my git log looks like below.

gitDeepDive> git log --oneline --graph
*   a3d0879 (HEAD -> master) Merge branch 'dev'
|\
| * 867d90c (dev) update hello.txt
| * 367c2d0 Update news
* | c36490a update docs
|/
* 2adbcac (test) Add doc folder and files
* 16f1fa8 commit hello.txt

Let's reset master branch to previous commit.

gitDeepDive> git switch master
Already on 'master'
gitDeepDive> git reset --hard c36490a
HEAD is now at c36490a update docs
gitDeepDive> git log --oneline --graph --all
* c36490a (HEAD -> master) update docs
| * 867d90c (dev) update hello.txt
| * 367c2d0 Update news
|/
* 2adbcac (test) Add doc folder and files
* 16f1fa8 commit hello.txt

Rebase

I rebase the dev branch. So I need to go to dev branch first.

git switch dev

Then I run git rebase command. As I rebase from the last commit of master, I can do git rebase master but I explicitly specify the commit id instead to illustrate that we can rebase from certain commit.

I see conflict error. Let's a closer look now.

gitDeepDive> git rebase c36490a
error: could not apply 367c2d0... Update news
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 367c2d0... Update news
Auto-merging docs/news.txt
CONFLICT (content): Merge conflict in docs/news.txt

It says "could not apply 367c2d0... Update news". This means git try to apply the changes of commit 367c2d0 to create new commit. Let's resolve the conflict. I simply take both changes here.

git mergetool

Alt Text

Once I resolved news.txt conflict, continue the rebase. I can edit the commit message. I simply accept default message and completed but I have another conflict.

gitDeepDive> git rebase --continue                                                        [detached HEAD 8bd3445] Update news
 1 file changed, 1 insertion(+)
error: could not apply 867d90c... update hello.txt
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 867d90c... update hello.txt
Auto-merging hello.txt
CONFLICT (content): Merge conflict in hello.txt

It says "could not apply 867d90c... update hello.txt". git apply next commit 867d90c on top of current commit. Simply resolve the conflict and continue.

gitDeepDive> git rebase --continue                                                        [detached HEAD 43df8ff] update hello.txt
 1 file changed, 2 insertions(+)
Successfully rebased and updated refs/heads/dev.

Let's check the log now.

gitDeepDive> git log --oneline --graph --all
* 43df8ff (HEAD -> dev) update hello.txt
* 8bd3445 Update news
* c36490a (master) update docs
* 2adbcac (test) Add doc folder and files
* 16f1fa8 commit hello.txt

Alt Text

So this is exactly what I wanted.

How to redo the rebase

I sometimes need to "redo" the rebase if I mistake on conflict resolution. As git doesn't delete objects items immediately, I still can go back to original state. Simply reset to original commit id will do the undo.

gitDeepDive> git reset 867d90c
Unstaged changes after reset:
M       docs/news.txt
M       hello.txt
gitDeepDive> git log --oneline --graph --all
* c36490a (master) update docs                                                            | * 867d90c (HEAD -> dev) update hello.txt
| * 367c2d0 Update news
|/
* 2adbcac (test) Add doc folder and files
* 16f1fa8 commit hello.txt

As you can see, it simply updates the commit id information in ..git\refs\heads\dev.

From here, I can do rebase again. If you really want to remove any orphaned objects, do git prune afterward but I usually don't.

Why people confuse rebase

This time, it was easy as there are only two commits to apply and each commit has different conflict. However, in real world scenario, I may need to keep resolving the same file again and again, as I keep updating the same file for each commit.

The reason why git have to apply each commit one by one, rather than apply the latest commit is to keep the history.

Squash commits

If you don't need all the commit history, you can "squash" commits. By squashing, I can create another commit which has all the changes in certain ranges.

I will create new commit which contains both 367c2d0 and 867d90c.
Alt Text

Make sure to reset to commit 867d90c on dev branch.

gitDeepDive> git switch dev
Switched to branch 'dev'
gitDeepDive> git reset --hard 867d90c
HEAD is now at 867d90c update hello.txt
gitDeepDive> git log --oneline --graph --all
* c36490a (master) update docs
| * 867d90c (HEAD -> dev) update hello.txt
| * 367c2d0 Update news
|/
* 2adbcac (test) Add doc folder and files
* 16f1fa8 commit hello.txt
gitDeepDive> 

Now run rebase with -i parameter and specify 2adbcac as rebase commit id.

git rebase -i 2adbcac 

git opens an editor to let me decide how I want to treat each commit. I have many options here.
Alt Text

I mark "squash" for the commit 867d90c so that I can combine this into 367c2d0.
Alt Text

Once I save the file, git open another editor so that I can edit commit message. I just accept the default one. Once rebase completed, check the log.

gitDeepDive> git log --oneline --graph --all
* 6a695b7 (HEAD -> dev) Update news                                                       | * c36490a (master) update docs                                                          |/
* 2adbcac (test) Add doc folder and files
* 16f1fa8 commit hello.txt

As I explained new commit is created. When I dig into commit file, I can see newly created commit and original commit reference same blob. I only check hello.txt here but you can check news.txt and article.txt

gitDeepDive> git cat-file -p edbfe82
040000 tree 2baf027b74c551817c2a5ef6a3472ccc8e99738c    docs
100644 blob 7a218e826670e77d05c1c244b514a7f449056752    hello.txt
gitDeepDive> git cat-file -p 867d90c
tree edbfe822158fb3be005aca22897a0395fa2c9252
parent 367c2d000be0ffbb640252384c820ce472fe32a4
author Kenichiro Nakamura <kenakamu@microsoft.com> 1588842488 +0900
committer Kenichiro Nakamura <kenakamu@microsoft.com> 1588842488 +0900

update hello.txt
gitDeepDive> git cat-file -p edbfe82
040000 tree 2baf027b74c551817c2a5ef6a3472ccc8e99738c    docs
100644 blob 7a218e826670e77d05c1c244b514a7f449056752    hello.txt

Now my commit chain looks like this.
Alt Text

Of course we can do rebase from c36490a rather than 2adbcac. Try it by yourself to see how it works.

Summary

Rebase is nothing special from git point of view, but as it may apply multiple commits one by one, it maybe a bit confusing. I explain cherry-pick in the next article.

Go to next article

Discussion

pic
Editor guide