In the Git version control system, there are two ways to combine one branch with another, represented by different commands:
git merge
. Commits from one branch are transferred into another by creating a merge commit.
git rebase
. Commits from one branch are transferred into another branch while preserving the original order of changes.
Simply put: with git merge
, the commits from one branch are “squashed” into one, while with git rebase
they remain untouched, yet the branches are combined.
Thus, the git rebase
command allows you to combine commits from both branches by forming a shared history of changes.
This guide will cover the git rebase
command, which is responsible for rebasing commits (changes) from one branch to another.
All the examples shown used Git version 2.34.1, running on a Hostman server with the Ubuntu 22.04 operating system.
You can use these guides to install Git on your machine:
The best way to understand how rebasing works in Git is to look at an abstract repository consisting of several branches. At the same time, the rebasing operation should be considered step by step.
Let’s assume we created a repository with a single branch master
, containing just one commit. The master
branch looks like this:
master
commit_1
Then, based on master
, we created a new branch hypothesis
, where we decided to test some features. In this new branch, we made several commits improving the code. The branch now looks like this:
hypothesis
commit_4
commit_3
commit_2
commit_1
Later, we added another commit to the master
branch, urgently fixing a vulnerability. The master
branch now looks like this:
master
commit_5
commit_1
Now our repository has two branches:
master
commit_5
commit_1
hypothesis
commit_4
commit_3
commit_2
commit_1
The master
branch is the main branch, while hypothesis
is secondary (derived). Later commits are listed above earlier ones, similar to the output of the git log
command.
Let’s say we want to continue working on the feature we had previously moved into the separate hypothesis
branch. However, this branch does not contain the critical vulnerability fix we made in master
.
Therefore, we want to “synchronize” the state of hypothesis
with master
so that the fix commit also appears in the feature branch. In other words, we want the repository structure to look like this:
master
commit_5
commit_1
hypothesis
commit_4
commit_3
commit_2
commit_5
commit_1
As you can see, the hypothesis
branch now exactly repeats the history of master
, even though it was originally created before commit_5
. In other words, hypothesis
now contains the history of both branches—its own and master
.
To achieve this, we need to rebase using the git rebase
command.
Afterward, the changes made in hypothesis
can be merged into master
using the classic git merge
command, which creates a merge commit.
Then the repository structure will look like this:
master
commit_merge
commit_5
commit_1
hypothesis
commit_4
commit_3
commit_2
commit_5
commit_1
Moreover, running git merge
after git rebase
can reduce the likelihood of conflicts.
Now that we’ve covered the theoretical aspect of the git rebase
command, we can move on to testing it in a real repository of an improvised project. The repository structure will repeat the earlier theoretical example.
First, let’s create a separate directory to hold the repository:
mkdir rebase
Then move into it:
cd rebase
Now we can initialize the repository:
git init
In the console, a standard informational message should appear:
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
...
And in the current directory, a hidden .git
folder will appear, which you can view with the following command:
ls -a
The -a
flag means all
and allows viewing the file system in extended mode. Its contents will be:
. .. .git
Before making commits, we need to specify some basic user information.
First, the name:
git config --global user.name "NAME"
Then the email:
git config --global user.email "NAME@HOST.COM"
Using simple text files, we will simulate adding different functions to the project. Each new function will be represented as a separate commit.
Create a file for the first improvised function:
nano function_1
Fill it with the content:
Function 1
Now index the changes made in the repository:
git add .
Just in case, check the indexing status:
git status
In the console, the following message should appear, showing the indexed changes:
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: function_1
Now we can commit:
git commit -m "commit_1"
The console should display a message confirming the successful commit into master:
[master (root-commit) 4eb7cc3] commit_1
1 file changed, 1 insertion(+)
create mode 100644 function_1
Now let’s create a new branch called hypothesis
:
git checkout -b hypothesis
The -b
flag is necessary to switch immediately to the new branch.
The console will display a confirmation message:
Switched to a new branch 'hypothesis'
Next, we need to make three commits in sequence with three files, similar to master:
commit_2
with file function_2
and content: Function 2
commit_3
with file function_3
and content: Function 3
commit_4
with file function_4
and content: Function 4
If we then check the commit list:
git log --oneline
The console will display the following sequence:
d3efb82 (HEAD -> hypothesis) commit_4
c9f57b7 commit_3
c977f16 commit_2
4eb7cc3 (master) commit_1
Here, the --oneline
flag is used to display commit information in a compressed single-line format.
The last step is to add another commit to the main branch master
. Let’s switch to it:
git checkout master
The console will confirm the switch:
Switched to branch 'master'
Now create another improvised function file:
nano function_5
With the following content:
Function 5
Next, index the changes:
git add .
And make the new commit:
git commit -m "commit_5"
If we check the current commit list:
git log --oneline
The master branch will now have two commits:
3df7a00 (HEAD -> master) commit_5
4eb7cc3 commit_1
To perform rebasing, first you need to switch to the hypothesis
branch:
git checkout hypothesis
And run the rebase:
git rebase master
After this, the console will display a message confirming a successful rebase:
Successfully rebased and updated refs/heads/hypothesis.
Now you can check the commit list:
git log --oneline
The console will show a list containing the commits of both branches in the original order:
8ecfd58 (HEAD -> hypothesis) commit_4
f715aba commit_3
ee47470 commit_2
3df7a00 (master) commit_5
4eb7cc3 commit_1
Now the hypothesis
branch contains the complete history of the entire repository.
Just like with git merge
, when using the git rebase
command, conflicts may occur that require manual resolution.
Let’s modify our repository in such a way that we artificially create a rebase conflict.
Create another file in the hypothesis
branch:
nano conflict
And write the following text into it:
There must be a conflict here!
Index the changes:
git add .
And make another commit:
git commit -m "conflict_1"
Now switch to the master
branch:
git checkout master
Create a similar file:
nano conflict
And fill it with the following content:
There must NOT be a conflict here!
Again, index the changes:
git add .
And make a commit:
git commit -m "conflict_2"
Reopen the created file:
nano conflict
And change its content to:
There definitely must NOT be a conflict here!
Again, index the changes:
git add .
And make another commit:
git commit -m "conflict_3"
Now switch back to the hypothesis
branch:
git checkout hypothesis
And perform another rebase:
git rebase master
The console will display a conflict message:
Auto-merging conflict
CONFLICT (add/add): Merge conflict in conflict
error: could not apply 6003ed7... conflict_1
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 6003ed7... conflict_1
Git suggests editing the conflict
file, indexing the changes with git add
, and then continuing the rebase using the --continue
flag.
That’s exactly what we will do:
nano conflict
The file will contain two conflicting versions wrapped in special markers:
<<<<<<< HEAD
There definitely must NOT be a conflict here!
=======
There must be a conflict here!
>>>>>>> 6003ed7 (conflict_1)
Our task is to remove the unnecessary parts and fill the file with a final version of arbitrary text:
There must absolutely definitely unanimously NOT be any conflict here!
Now index the changes:
git add .
And continue the rebase:
git rebase --continue
After this, the console will open a text editor suggesting you modify the original commit message of the commit where the conflict occurred:
conflict_1
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# interactive rebase in progress; onto bd7aefc
# Last commands done (4 commands done):
# pick 8ecfd58 commit_4
# pick 6003ed7 conflict_1
# No commands remaining.
# You are currently rebasing branch 'hypothesis' on 'bd7aefc'.
#
# Changes to be committed:
# modified: conflict
#
The console will then display a message about the successful completion of the rebase process:
[detached HEAD 482db49] conflict_1
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/hypothesis.
Now if you check the commit list in the hypothesis branch:
git log --oneline
You will see the original sequence of all changes made:
482db49 (HEAD -> hypothesis) conflict_1
bd5d036 commit_4
407e245 commit_3
948b41c commit_2
bd7aefc (master) conflict_3
d98648d conflict_2
3df7a00 commit_5
4eb7cc3 commit_1
Notice that the commits conflict_2
and conflict_3
, made in the master
branch, appear in the history earlier than the conflict_1
commit. However, this applies to any commits made in the master
branch.
In addition to working with local branches, rebasing can be done when pulling changes from a remote repository. To do this, you need to add the --rebase
flag to the standard pull
command:
git pull --rebase remote branch
Where:
remote
is the remote repository
branch
is the remote branch
Essentially, this pull configuration is equivalent to git rebase
, except that the changes (commits) applied to the current branch are taken from the remote repository.
The git rebase
command makes it possible to form a fairly linear history of the target branch, consisting of sequentially made commits. Such a sequence without branching makes the history easier to perceive and understand.
Running git rebase
beforehand can significantly reduce the likelihood of conflicts when merging branches with git merge
. Conflicts are easier to resolve in sequential commits rather than in commits merged into a single merge commit. This is especially relevant when pushing branches to remote repositories.
Unlike merging, rebasing partially rewrites the history of the target branch, removing unnecessary history elements.
The ability to significantly restructure commit history can lead to irreversible errors in the repository. This means that some data may be permanently lost.
Merging two branches using rebasing, implemented with the git rebase
command, is fundamentally different from the classic merge done with the git merge
command.
git merge
turns the commits of one branch into a single commit in another.
git rebase
moves commits from one branch to the end of another while preserving the original order.
A similar rebasing effect can also be achieved when using the git pull
command with the additional --rebase
flag.
On one hand, the git rebase
command allows you to achieve a cleaner and more understandable commit history in the main branch, which increases the maintainability of the repository.
On the other hand, git rebase
reduces the level of detail in changes within the branch, simplifying the history and removing some of its records.
For this reason, rebasing is a feature intended for more experienced users who understand how Git works.
Most often, the git rebase
command is used together with the git merge
command, allowing you to achieve the most optimal repository and branch structure.