Baeldung Pro – Ops – NPI EA (cat = Baeldung on Ops)
announcement - icon

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.

1. Overview

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.

2. Understanding Merge Strategies

Before we get into the various merge strategies, let’s first look at what merging in Git is.

2.1. What Is a Merge in Git?

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.

2.2. Fast-Forward Merge

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

2.3. Non-Fast-Forward Merge

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.

3. Types of Merge Strategies

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.

3.1. Resolve Strategy

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.

3.2. Recursive 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

3.3. Octopus Strategy

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.

3.4. Ours Strategy

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.

3.5. Subtree Strategy

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.

4. The Modern Merge-Ort Strategy

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.

5. Choosing the Right Merge Strategy

Knowing when to use each merge strategy helps maintain smooth collaboration and a clean project history. Here are some key factors to consider:

  • Code complexity: Simple projects work with resolve or recursive; complex histories benefit from merge-ort or subtree.
  • Number of contributors: Large teams need strategies like merge-ort for handling renames, large histories, and frequent merges.
  • Conflict frequency: Frequent conflicts are best managed with recursive using options like -X ours or -X theirs.

6. Good Practices in Git Merges

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:

  • Using structured workflows like Git Flow or trunk-based development for clear branch management
  • Scheduling merges after milestones to minimize disruptions
  • Keeping branches focused on single tasks or features to reduce conflicts
  • Naming branches descriptively, such as feature/login-system or bugfix/typo-fix. This makes it easier to understand the context of the branch.

7. Advanced Git Merge Techniques

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.

8. Conclusion

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.