<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Khanh Nguyen</title>
    <description>The latest articles on Forem by Khanh Nguyen (@khanhnh).</description>
    <link>https://forem.com/khanhnh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3628121%2Fc061d93c-3b25-40f9-9e90-873433f67956.png</url>
      <title>Forem: Khanh Nguyen</title>
      <link>https://forem.com/khanhnh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/khanhnh"/>
    <language>en</language>
    <item>
      <title>Understanding Non-Fast-Forward Merges in Git</title>
      <dc:creator>Khanh Nguyen</dc:creator>
      <pubDate>Tue, 25 Nov 2025 02:13:26 +0000</pubDate>
      <link>https://forem.com/khanhnh/understanding-non-fast-forward-merges-in-git-3no0</link>
      <guid>https://forem.com/khanhnh/understanding-non-fast-forward-merges-in-git-3no0</guid>
      <description>&lt;p&gt;Git merges are a fundamental part of collaborative development, but not all merges are created equal. While fast-forward merges are simple and linear, non-fast-forward merges play a crucial role in preserving project history and maintaining a clear record of feature development. Let's explore what non-fast-forward merges are, when to use them, and why they matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a Non-Fast-Forward Merge?
&lt;/h2&gt;

&lt;p&gt;A non-fast-forward merge occurs when Git creates a new merge commit to combine two branches, even when a fast-forward merge would be possible. This merge commit has two parent commits: one from each branch being merged.&lt;/p&gt;

&lt;p&gt;In contrast, a fast-forward merge simply moves the branch pointer forward along a linear path when the target branch hasn't diverged from the source branch. No merge commit is created in this case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Anatomy of a Non-Fast-Forward Merge
&lt;/h2&gt;

&lt;p&gt;Consider this scenario: You create a feature branch from main, make some commits, and meanwhile no new commits are added to main. When you merge:&lt;/p&gt;

&lt;h3&gt;
  
  
  Fast-Forward Merge
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before merge:
    A---B---C     (main)
             \
              D---E---F     (feature)

After fast-forward merge:
    A---B---C---D---E---F     (main, feature)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git simply moves the main branch pointer to the tip of your feature branch. The history remains linear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Non-Fast-Forward Merge
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before merge:
    A---B---C     (main)
             \
              D---E---F     (feature)

After non-fast-forward merge (--no-ff):
    A---B---C-----------M     (main)
             \         /
              D---E---F     (feature)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git creates a new merge commit (M) that combines the branches, preserving the fact that development happened on a separate branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Perform a Non-Fast-Forward Merge
&lt;/h2&gt;

&lt;p&gt;To force a non-fast-forward merge, use the &lt;code&gt;--no-ff&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout main
git merge &lt;span class="nt"&gt;--no-ff&lt;/span&gt; feature-branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a merge commit even when a fast-forward would be possible. Git will open your default editor to let you customize the merge commit message.&lt;/p&gt;

&lt;p&gt;You can also configure Git to always use non-fast-forward merges for specific branches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config branch.main.mergeoptions &lt;span class="s2"&gt;"--no-ff"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real-World Example: Feature Development
&lt;/h2&gt;

&lt;p&gt;Let's look at a more realistic scenario with multiple features being developed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;With fast-forward merges:
    A---B---C---D---E---F---G---H---I     (main)

    Where: D,E,F = Feature 1 commits
           G,H,I = Feature 2 commits

With non-fast-forward merges:
    A---B---C-------M1------M2     (main)
             \     /  \     /
              D---E    F---G
           Feature 1  Feature 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how the non-fast-forward approach clearly shows two distinct features merged into main, while the fast-forward approach loses this context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits of Non-Fast-Forward Merges
&lt;/h2&gt;

&lt;p&gt;Non-fast-forward merges offer several advantages for project management and history tracking.&lt;/p&gt;

&lt;p&gt;First, they preserve feature boundaries. Each feature remains visually distinct in the commit graph, making it easy to see where a feature started and ended. This grouping is invaluable when reviewing project history or tracking down issues.&lt;/p&gt;

&lt;p&gt;Second, they simplify rollbacks. If a feature needs to be reverted, you can simply revert the single merge commit rather than identifying and reverting multiple individual commits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reverting a feature with non-fast-forward merge:
    A---B---C-------M1------M2------R     (main)
             \     /  \     /       |
              D---E    F---G        |
                                    └── Revert of M1

vs. trying to revert individual commits:
    A---B---C---D---E---F---G---H---I---R1---R2---R3     (main)
                └───┴───┘
            Need to identify and revert
            these specific commits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Third, they provide better documentation. The merge commit message can describe what the feature accomplishes at a high level, while individual commits show the implementation details.&lt;/p&gt;

&lt;p&gt;Fourth, they maintain a cleaner main branch. The main branch contains primarily merge commits representing completed features, making it easier to understand the project's evolution at a glance.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use Non-Fast-Forward Merges
&lt;/h2&gt;

&lt;p&gt;Non-fast-forward merges are particularly valuable in certain workflows and situations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Git Flow Workflow
&lt;/h3&gt;

&lt;p&gt;In Git Flow, non-fast-forward merges are standard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    master    A-----------M1----------M2
               \         /  \        /
    develop     B---C---D----E---F--G
                 \     /       \   /
    feature/1     H---I         J-K
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure clearly shows releases (M1, M2) and feature integrations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pull Request / Merge Request Workflows
&lt;/h3&gt;

&lt;p&gt;Many teams use non-fast-forward merges when closing pull requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    main      A---B---------M1--------M2
                   \       /  \       /
    PR #123         C-----D    \     /
                                 \   /
    PR #124                       E-F
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each merge commit corresponds to a completed pull request, providing a clear audit trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Potential Drawbacks
&lt;/h2&gt;

&lt;p&gt;While powerful, non-fast-forward merges aren't always the best choice.&lt;/p&gt;

&lt;p&gt;They create a more complex commit graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Complex graph with many features:
    A---B---M1---M2---M3---M4---M5     (main)
       |   /|   /|   /|   /|   /
       |  / |  / |  / |  / |  /
       |/   |/   |/   |/   |/
       C    D    E    F    G

vs. Linear history:
    A---B---C---D---E---F---G     (main)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For developers new to Git, the complex graph can be harder to understand than a linear history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;p&gt;To get the most from non-fast-forward merges, consider these practices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clean Feature Branches Before Merging
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before cleanup:
    main      A---B---C
                   \
    feature         D---E(WIP)---F(fix typo)---G

After cleanup (squash/rebase):
    main      A---B---C
                   \
    feature         D'---E'

After merge:
    main      A---B---C-------M
                   \         /
    feature         D'-----E'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you clean, meaningful commits within the feature while preserving the merge boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Descriptive Merge Messages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Good merge commit message:
    Merge: Add user authentication system (#PR-123)

    - Implements JWT-based authentication
    - Adds login/logout endpoints
    - Includes password reset functionality

    Closes #456, #457

Default merge commit message:
    Merge branch 'feature-auth'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Visual Tools
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;git log --graph --pretty=oneline --abbrev-commit&lt;/code&gt; to visualize your branch structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* 5d3f1a2 (HEAD -&amp;gt; main) Merge: Add payment processing
|\  
| * 8c4b2e1 Add Stripe integration
| * 7a9c3f2 Create payment models
|/  
* 3e2d1c5 Merge: User authentication system
|\  
| * 9f8e7d6 Add JWT tokens
| * 2b3c4d5 Create login endpoints
|/  
* 1a2b3c4 Initial commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Non-fast-forward merges are a powerful tool for maintaining a clear and informative project history. While they add complexity to the commit graph, they provide valuable context about feature development, simplify rollbacks, and support collaborative workflows.&lt;/p&gt;

&lt;p&gt;The choice between fast-forward and non-fast-forward merges ultimately depends on your team's needs and preferences. For projects that benefit from clear feature boundaries and comprehensive history, non-fast-forward merges are often the right choice. Understanding when and how to use them effectively will make you a more capable Git user and a better collaborator.&lt;/p&gt;

</description>
      <category>git</category>
      <category>gitflow</category>
    </item>
  </channel>
</rss>
