datarekha

Merging

How to fold a finished feature branch back into main safely, and what makes fast-forward merges different from true merge commits.

8 min read Intermediate Git Lesson 8 of 15

What you'll learn

  • git merge feature: run it from the target branch, not the feature branch
  • Fast-forward vs 3-way merge: what history each strategy produces and when Git chooses each
  • How to force a merge commit with --no-ff and clean up merged branches

Before you start

The last lesson left your feature branch full of commits while main moved on without it — two diverging lines of history. This lesson is how Git weaves them back into one. The twist is that “merge” is really two different operations wearing a single command name, and which one you get depends entirely on whether main moved while you were away.

Setting the scene

You have been working on a feature branch. main exists in parallel. At some point the two branches diverge — or they don’t. That single fact determines which of two very different merge strategies Git uses.

Fast-forward merge

A fast-forward merge happens when the target branch (the one you want to merge into) has not received any new commits since the feature branch was created. In other words, the feature branch is simply ahead of main on a straight line.

Git has nothing to reconcile. It moves the main pointer forward to the tip of feature. No new commit is created. The word “fast-forward” is Git telling you: “I just slid the label along — I didn’t have to think.”

# You are on main, which has not moved since feature branched off
git checkout main
git merge feature
Updating 3f1a2b4..c7e9d01
Fast-forward
 src/auth.ts | 42 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)

Notice the word Fast-forward in Git’s output — that is your confirmation.

3-way merge (true merge)

A 3-way merge is required when both branches have advanced since they diverged. Main has new commits; the feature branch has new commits. They share a common ancestor — the last commit that both branches have in common — but they have gone different directions since then.

Git uses three snapshots to work out what the combined result should look like:

  1. The common ancestor (the fork point)
  2. The tip of main
  3. The tip of feature

Because no single pointer move can represent both lines of work, Git creates a brand-new merge commit. A merge commit is special: it has two parents — one pointing back into main’s history and one pointing back into feature’s history. It is the only commit type that stitches two lines together.

# Both branches have new commits since they diverged
git checkout main
git merge feature
Merge made by the 'ort' strategy.
 src/payment.ts | 27 +++++++++++++++++++
 1 file changed, 27 insertions(+)

Git will open your editor so you can write (or accept the default) merge commit message. The result looks like the diagram below.

Before and after a 3-way merge

BEFOREA (ancestor)shared baseB1main tipF1feature tipAFTER git merge featureA (ancestor)shared baseB1main tipF1feature tipM (merge commit)two parents: B1 + F1← main (HEAD)

Before the merge, B1 (main) and F1 (feature) diverged from a common ancestor A. After git merge feature, Git creates merge commit M whose two parents are B1 and F1.

Forcing a merge commit with --no-ff

If the branches have not diverged, Git will fast-forward by default. Sometimes that is not what you want — you may want a permanent record in history that a feature branch existed and was merged as a unit.

--no-ff (no fast-forward) tells Git to always create a merge commit, even when a fast-forward would be possible:

git checkout main
git merge --no-ff feature
Merge branch 'feature' into main
# (editor opens for commit message)

Many teams enable this as policy so every feature shows up as a distinct bubble in the commit graph, making history easier to audit.

Cleaning up after a merge

Once the merge is done and pushed, the feature branch has served its purpose. Delete it to keep the repository tidy:

# Delete the local branch (safe — Git refuses if unmerged changes exist)
git branch -d feature

# Verify which branches are already merged into main
git branch --merged main
* main
  feature

Any branch listed by git branch --merged is safe to delete — its commits are already reachable from main. A branch that is not listed has work that has not been merged yet; -d will refuse to delete it, protecting you from accidental data loss.

Quick reference

ScenarioCommandResult
Merge feature into current branchgit merge featureFast-forward or 3-way
Always create a merge commitgit merge --no-ff featureMerge commit regardless
Delete merged branchgit branch -d featureRemoves local branch
List merged branchesgit branch --mergedSafe-to-delete list

In one breath

git merge feature brings a branch’s work into whatever branch you are currently on — so you checkout main first, never the reverse. Git then picks one of two strategies by topology: if main has not moved since feature branched, it does a fast-forward — no new commit, it just slides the main pointer up to feature’s tip. If both lines diverged from a common ancestor, it does a 3-way merge, creating a merge commit with two parents that stitches the histories together. You can force that second form even when a fast-forward is possible with --no-ff (many teams do, so each feature shows as a distinct bubble in the graph). Afterward, git branch -d feature tidies up — and safely refuses if the work is not actually merged.

Practice

Before the quiz, predict the topology twice. First: you branch feature off main, commit twice on it, and meanwhile nobody touches main; you git checkout main && git merge feature. Fast-forward or merge commit — and why? Now replay it with one change: a teammate landed a commit on main before you merged. Which strategy now, and how many parents does the resulting tip commit have?

Quick check

0/3
Q1You run `git merge feature` and Git prints `Fast-forward`. What does this tell you?
Q2What is the defining characteristic of a merge commit?
Q3Your team uses `git merge --no-ff` for every feature merge, even when fast-forward would be possible. A new teammate asks why. What is the main benefit?

A question to carry forward

Every merge in this lesson quietly assumed the best case: the two branches touched different lines, so Git blended them automatically and you never saw the machinery. But branches are not always so polite. What happens when main changed line 12 of auth.py to one thing and your feature changed that exact same line to something else? Git has two competing edits and no way to know which is right — so it stops, mid-merge, and hands the decision to you. Learning to read that standoff and resolve it calmly — instead of panicking and --abort-ing every time — is the next lesson: merge conflicts.

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Practice this in an interview

All questions
How does DVC differ from a feature store, and when would you reach for each?

DVC (and lakeFS) version raw datasets and model artifacts as immutable snapshots tied to Git commits, giving reproducibility and rollback. A feature store manages computed features for training and serving, its main job being to keep offline and online feature definitions in sync to prevent training-serving skew. They are complementary: DVC answers what data made this model, while a feature store answers how do I serve the same features consistently.

Why isn't a git commit enough to reproduce an ML training run?

A git commit captures code, but an ML run also depends on the exact training data, hyperparameters, environment, and randomness, none of which live in Git. Datasets are too large for Git and change independently of code, so you need a data-versioning tool like DVC or lakeFS to pin a content hash of the data to the commit. Full reproducibility means versioning code, data, config, environment, and seeds together and linking them.

Merge two sorted linked lists into one sorted list.

Use a dummy head node to avoid special-casing the result's first element. Walk both lists with two pointers, always appending the smaller current node to the result. When one list is exhausted, append the remainder of the other. O(n + m) time, O(1) space.

What is the difference between merge, join, and concat in pandas?

concat stacks DataFrames along an axis without matching keys; join aligns on the index (or a single key column) using a convenient shorthand; merge is the most general, joining on any column(s) with full SQL-style control over the join type, key names, and suffix handling.

Related lessons

Explore further

Skip to content