Learn Git - Merging Code (Basic Merge vs Rebase)
Episode 7 of 21

Learn Git - Merging Code (Basic Merge vs Rebase)

Dissecting code-merging mechanisms: fast-forward merge, 3-way merge commits, the --no-ff option, the causes of merge conflicts along with the anatomy of their markers, and the steps to resolve conflicts manually then commit the result.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

In episode 6 you learned that a branch is just a lightweight pointer to a commit, and you started creating many parallel development paths. But parallelism without bridges is just chaos. The point where two paths meet again is what determines how clean a project's history is.

Code merging is the skill most often tested in the real working world: every Pull Request on GitHub is essentially a merge. In this episode we dissect the two main mechanisms — merge and basic rebase — then defuse the scary myth called the merge conflict, complete with a real example and the steps to resolve it.

The Merge Mechanism

Fast-Forward Merge

The simplest case: you are on main, then create the fitur-nav branch and commit twice on it. Meanwhile main has not moved at all. Because the paths have not diverged, Git can simply slide the pointer main forward to the latest position of fitur-nav — without creating an extra commit.

Fast-forward merge
git switch main
git merge fitur-nav

The output mentions "Fast-forward", and the history stays linear like a train running on the same track. No new commit, just the pointer moving.

Non-Fast-Forward Merge (3-Way Merge Commit)

Now the more common case: main and fitur-nav both moved after the divergence point. They are both ahead of each other, so neither can just slide its pointer.

For this case Git performs a 3-way merge: it compares three points — the commit on our branch (HEAD), the commit of the merged branch, and the common ancestor commit (the last point where both were still on one path). The results are combined into a single new merge commit with two parents.

Diverged history after the merge
*   f0a2b1c Merge branch 'fitur-nav'
|\
| * 7c3d9e1 feat: tambah navbar
| * 2b8a4f2 feat: tambah komponen nav
* | a1c5e7d fix: perbaiki header

Forcing a Merge Commit with --no-ff

Even when fast-forward is possible, sometimes you want a branch's existence to always be recorded in history. git merge --no-ff <nama-branch> forces a merge commit even when fast-forward is possible. This pattern is common in teams that want features to appear as clear units in history.

Forcing a merge commit
git merge --no-ff fitur-nav

Merge Conflicts: Why Do They Happen?

Before fearing conflicts, first understand when they occur. Git is very smart at merging: if two branches change different lines in the same file, Git merges them automatically without drama. Conflicts only appear when two branches change the same (or adjacent) lines in the same file, or one deletes a file that another branch modified.

Simply put: Git cannot determine which is correct because both claim the same area. At this point Git asks for a human decision.

Note

A conflict is not a sign you are using Git wrong — it is a sign that two legitimate changes intersect. Professional teams resolve dozens of conflicts every week calmly.

Anatomy of Conflict Markers

When a conflict occurs, Git inserts markers inside the file:

Anatomy of a merge conflict
<<<<<<< HEAD
Versi dari branch yang sedang aktif (main)
=======
Versi dari branch yang digabung (fitur-nav)
>>>>>>> fitur-nav

Three important parts:

  • <<<<<<< HEAD — start of the block with our active branch's version.
  • ======= — separator between the two versions.
  • >>>>>>> fitur-nav — end of the block with the merged branch's version.

Resolving Conflicts Step by Step

Let's work through a real example. A project aplikasi-kafe has an app.js file with the line that determines the app name. The main branch changed it to KafeKita, while the fitur-rebrand branch changed the same line to Kafe Kopi Nusantara. When merging:

Trying to merge a branch
git switch main
git merge fitur-rebrand

Git responds with a conflict status. The resolution steps:

  1. Identify the problematic file with git status — the file will be labeled "both modified".
  2. Open the file in your editor. Inside it there is a conflict block:
The app.js file in a conflicted state
<<<<<<< HEAD
const nama = "KafeKita";
=======
const nama = "Kafe Kopi Nusantara";
>>>>>>> fitur-rebrand
  1. Choose the final result. For a rebranding, we choose the new name, then remove all conflict markers so only the desired line remains:
The app.js file after resolution
const nama = "Kafe Kopi Nusantara";
  1. Mark it as resolved with git add app.js, then finish the merge:
Finishing the merge after a conflict
git add app.js
git merge --continue

git merge --continue creates the merge commit and opens the editor for the commit message (git commit also works). When done, check with git log --oneline that the merge commit was created.

Tip

In VS Code, the "Accept Current / Accept Incoming / Accept Both" buttons help you choose a version quickly. For complicated conflicts, use a visual diff tool from the editor or git mergetool.

Basic Rebase: A Linear History Alternative

Besides merge, there is rebase: taking the feature branch's commits and reapplying them on top of the tip of the main branch, so the history appears linear, as if the feature were worked on sequentially after main.

Rebasing a feature branch on top of main
git switch fitur-rebrand
git rebase main

The difference: merge preserves the original divergence and records a merge commit; rebase rewrites commit hashes so the history line is straight. Rebase can also raise the same conflicts, resolved with git add then git rebase --continue.

Warning

Golden rule: never rebase a public branch shared with other people. Rebase rewrites history; if others have already pulled the old commits, the new hashes will break their work. Use rebase only for local/private branches — advanced details are in episode 16.

Closing

Key points from this episode:

  • Fast-forward merge slides the pointer without a new commit; 3-way merge produces a merge commit with two parents.
  • git merge <nama-branch> merges a branch; --no-ff forces a merge commit.
  • Conflicts appear when two branches change the same lines; changes in different areas merge automatically.
  • Marker anatomy: <<<<<<< HEAD, =======, and >>>>>>> nama-branch.
  • Conflict resolution: choose the content, remove the markers, git add, then git merge --continue.
  • Rebase reapplies commits for a linear history; do not use it on public branches.

The local foundation is solid. In episode 8 we take all that code into the real world: working with remote repositories on GitHub — from creating a repository, linking it with origin, to the first push. See you there!

Learn Git - Merging Code (Basic Merge vs Rebase) | Learn Git & GitHub