Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: May 2, 2025
Choosing the right merge strategy when using Git can be difficult sometimes, especially when dealing with a fast-growing codebase. With multiple contributors working on new features, the chances of conflicts increase, demanding efficient merge processes.
It’s important to know which merge strategy to use at any given time. Each approach handles changes differently, and the strategy we choose affects not only how conflicts are resolved, but also the flow and clarity of our project history.
In this tutorial, we’ll discuss the common merge strategies.
Before we get into the various merge strategies, let’s first look at what merging in Git is.
In Git, a merge is the process of integrating changes from one branch into another. Typically, this happens when we conclude on a feature or fix a bug in a separate branch and want to integrate those updates into the main branch. As a result, this process unifies the changes from both branches into a single codebase.
To achieve this, Git identifies a common ancestor commit, compares the differences between the branches, and generates a new commit that reflects the integrated changes. This resulting commit is known as a merge commit.
There are two basic types of merges—fast-forward and non-fast-forward. Let’s discuss these merge types in the following sections.
The fast-forward merge occurs when the branch being merged is directly ahead of the branch we want to merge into. In this case, Git moves the target branch’s pointer to match the head of the source branch. This is usually quick and clean because it doesn’t create a new merge commit.
Let’s see how this works. In the terminal, let’s create and navigate to a new project directory and initialize the Git repository:
$ mkdir fast-forward-merge-demo && cd fast-forward-merge-demo
$ git init -b main
The -b main option initializes the repository with main as the default branch instead of master.
Next, we create a file and add some text to it:
$ echo "Initial content" > file.txt
Now, let’s stage the changes and make the first commit:
$ git add file.txt
$ git commit -m "Initial commit"
At this point, the repository has a single branch (main) with one commit. Let’s create a new branch for feature development:
$ git checkout -b feature-branch
Now, let’s add some content to the file and commit the changes:
$ echo "Feature content" >> file.txt
$ git add file.txt
$ git commit -m "Add feature content"
Let’s switch back to the main branch:
$ git checkout main
Now, we can merge the feature-branch into main. Since no commits were made to main during this time, Git performs a fast-forward merge:
$ git merge feature-branch
We should see an output similar to this:
Updating 40a1e54..63a3543
Fast-forward
file.txt | 1 +
1 file changed, 1 insertion(+)
Git updates the branch pointer from the previous commit (40a1e54) to the latest commit in the feature-branch (63a3543). It also shows that one file, file.txt, was changed.
Now, let’s check the commit history using:
$ git log --oneline
The above command should display a similar output to this:
63a3543 (HEAD -> main, feature-branch) Add feature content
40a1e54 Initial commit
A non-fast-forward merge occurs when the branches being merged have diverged, meaning both branches contain unique commits. In such cases, Git creates a new merge commit to combine the histories of the branches. This method is often used in collaborative workflows to retain the unique changes from each branch and mark the point where they’re merged.
Let’s start by creating a new repository and making the first commit:
$ mkdir non-fast-forward-merge-demo && cd non-fast-forward-merge-demo
$ git init -b main
This initializes a Git repository with main as the default branch.
Next, let’s create a file and add some content:
$ echo "Initial content" > file.txt
Let’s stage and commit the changes:
$ git add file.txt
$ git commit -m "Initial commit"
Now, we create a feature branch to add changes specific to the feature:
$ git checkout -b feature-branch
$ echo "Feature branch content" > feature.txt
$ git add feature.txt
$ git commit -m "Add feature-specific content"
With the feature branch ready, we can switch back to the main branch and make updates to it:
$ git checkout main
$ echo "Main branch content" > main.txt
$ git add main.txt
$ git commit -m "Add main-specific content"
Now, let’s merge the feature-branch into main. Since the branches have diverged, Git performs a non-fast-forward merge with a prompt to enter a merge commit message:
$ git merge feature-branch
Next, we should see a text editor like Vim to enter a commit message:
Merge branch 'feature-branch'
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
Let’s save and close the editor, Git automatically creates the merge commit after the message is saved.
To confirm that the merge was successful, let’s view the commit history using graph representation:
$ git log --graph --oneline
The above command should return a similar output to this:
* 314066a (HEAD -> main) Merge branch 'feature-branch'
|\
| * 42717db (feature-branch) Add feature content
* | 00a4df1 Add main-specific content
|/
* e8fffdb Initial commit
We’ll discuss the various types of merge strategies in the next section.
The right merge strategy ensures smoother collaboration and a well-structured commit history. With this in mind, let’s explore the common merge strategies and how to apply them.
The resolve strategy performs a simple three-way merge between two branches. It works well for straightforward scenarios with minimal conflicts but doesn’t support renamed files or merging more than two heads.
For instance, if we have two branches, main and feature-branch, each with unique commits, we can merge feature-branch into main using the resolve strategy:
$ git checkout main
$ git merge -s resolve feature-branch
This command integrates the changes from feature-branch into main using the resolve strategy.
The recursive strategy is Git’s default for merging two branches. It uses a three-way merge and handles cases with diverging branch histories, including renames. It also provides options for resolving conflicts more effectively.
For example, to merge a develop branch into main, Git automatically applies the recursive strategy:
$ git checkout main
$ git merge develop
We can also apply options like ours or theirs to resolve conflicts by prioritizing changes from one branch. For example, to prioritize changes from the current branch during conflicts:
$ git merge -X ours develop
The octopus strategy enables the merging of multiple branches simultaneously. It’s efficient for combining simple changes from many branches. However, it doesn’t handle conflicts well.
For instance, to merge multiple branches (feature-1, feature-2, and feature-3) into main, we use:
$ git checkout main
$ git merge -s octopus feature-1 feature-2 feature-3
This command combines the changes from all three branches into main.
This strategy resolves merges by favoring the current branch’s changes, ignoring changes from the other branches. It’s useful when we want to preserve the current branch while discarding incoming changes.
For instance, we can use this to merge feature-xyz into main without including changes from feature-xyz:
$ git checkout main
$ git merge -s ours feature-xyz
This records the merge in history without applying the changes from the feature-xyz branch.
This strategy integrates external repositories or nested directories into a project while preserving their hierarchy.
For example, let’s merge an external GitHub repository, repo-xyz as a subtree into the current project:
$ git remote add repo-xyz https://github.com/username/repo-xyz.git
$ git fetch repo-xyz
$ git merge -s subtree --allow-unrelated-histories repo-xyz/main
This command integrates the external repository into the current project while maintaining its structure.
The merge-ort strategy was introduced in Git 2.34. It replaces the older recursive strategy as the default. Merge-ort is faster and more efficient, making it better suited for handling complex merges in large projects with long histories.
The recursive strategy processes file changes one at a time, which can slow down merges in repositories with large files, deep histories, or frequent renames. Merge-ort improves on this by using a data-driven approach, processing all changes in memory simultaneously. This significantly speeds up the merge process and reduces the computational load.
Knowing when to use each merge strategy helps maintain smooth collaboration and a clean project history. Here are some key factors to consider:
Managing merges in Git can be difficult in busy projects with multiple contributors. However, some practices can help avoid conflicts and keep the codebase clean:
We can leverage advanced merge options and automation to streamline workflows.
We can use -X patience to prioritize meaningful changes or -X diff-algorithm=histogram for better handling of large files:
$ git merge -X patience feature-branch
$ git merge -X diff-algorithm=histogram feature-branch
Also, we can integrate merges into CI/CD pipelines with tools like GitHub Actions to test and merge branches automatically after successful checks.
These ensure smoother workflows, cleaner histories, and better collaboration across teams.
In this article, we discussed Git’s merge strategies, starting with the basics of fast-forward and non-fast-forward merges and progressing to advanced strategies like recursive, octopus, subtree, and merge-ort. Each strategy serves a unique purpose, whether managing simple updates or handling complex branch histories in large projects.
We also discussed practical steps to handle merges and streamline workflows using automation and custom options. Understanding and applying the right strategies ensures smoother collaboration, cleaner project histories, and efficient code management.