Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

Git Fast-Forward vs True Merge: Commit-Graph Outcomes

Use real Git to distinguish fast-forward updates, true merge commits, --no-ff, and --ff-only by observing branch tips, object IDs, and parent relationships.

Levels: Intermediate ~35 min read

Intermediate

A fast-forward and a true merge change history in different ways

V436 established the ancestry question: when one branch tip is an ancestor of the other, Git can identify that relationship before integration. This lesson owns the concrete merge outcomes. A fast-forward moves the current branch tip to an existing descendant commit and creates no merge commit. A true merge is required when the histories have diverged; Git records the combined result in a new merge commit with the relevant branch tips as parents.

bash
git switch main
git merge topic

Quick check

If `topic` is already a descendant of the current `main` tip, what does the default merge do?

main is an ancestor of topic.main now points at the existing topic commit; no new merge commit was created.

Fast-forward: no new commit object is needed

A fast-forward is not a special kind of commit. It is a branch-reference update. If `main` points to A and `topic` points to B where A is an ancestor of B, integrating `topic` can simply move `main` from A to B. The commit graph itself is unchanged: B already exists. `HEAD` remains attached to `main`; the branch reference moves to an existing commit.

bash
before=$(git rev-parse main)
topic=$(git rev-parse topic)
git merge topic
printf 'main-before=%s\nmain-after=%s\ntopic=%s\n' "$before" "$(git rev-parse main)" "$topic"
git rev-list --parents -n 1 main

Quick check

After a successful fast-forward, which object was newly created by the merge itself?

`main` points to an ancestor of `topic`.`main` names the pre-existing `topic` commit; the commit graph has not gained a merge commit.

True merge: divergent tips require a new merge commit

When the current tip and the named tip are not in a direct ancestor/descendant relationship, a fast-forward cannot represent the integration. Git performs a true merge and records the resulting snapshot in a new merge commit. The merge commit has the current branch tip and the merged tip as parents. The exact file-level reconciliation can be clean or conflicting; conflict handling is owned by the later merge-conflict lessons.

bash
git switch main
git merge --no-edit topic
git rev-list --parents -n 1 HEAD
git show --no-patch --format=fuller HEAD

Quick check

In a non-fast-forward merge that completes successfully, what distinguishes the resulting merge commit?

The current and named tips have diverged and the merge completes.HEAD points to a new merge commit whose parent list records both sides.

`--no-ff` deliberately creates a merge commit even when fast-forward is possible

The default merge policy permits a fast-forward when possible. `git merge --no-ff topic` changes that policy: even if `topic` is a descendant of the current tip, Git creates a merge commit instead of simply moving the current branch to the existing descendant. This lesson treats `--no-ff` as an explicit history-shape choice, not as evidence that the histories were originally divergent.

bash
git switch main
git merge --no-ff --no-edit topic
git rev-list --parents -n 1 HEAD

Quick check

If fast-forward is possible, what does `git merge --no-ff topic` force?

`topic` is a descendant of `main`, so default merge could fast-forward.A new merge commit is recorded instead of moving `main` directly to the existing topic tip.

`--ff-only` makes the ancestry condition an explicit requirement

`git merge --ff-only topic` permits integration only when the named history can be represented by a fast-forward. If the histories have diverged, Git refuses to perform the merge and exits non-zero. This is a precondition check plus integration policy: it does not turn a divergent history into a merge commit.

bash
git merge --ff-only topic
printf 'exit=%s\n' $?

Quick check

What happens when `--ff-only` is requested but the histories have diverged?

The current and named tips are not in an ancestor relationship.No merge commit is created and the command reports failure.

The commit graph reveals the difference more precisely than a file snapshot

A final working-tree snapshot can look correct after either a fast-forward or a true merge. The durable difference is in the commit graph. In a fast-forward, the receiving branch adopts an existing commit. In a true merge, the receiving branch adopts a new commit with multiple parents. Use parent inspection and object IDs rather than only comparing files.

bash
git rev-parse HEAD
git rev-list --parents -n 1 HEAD
git log --graph --oneline --decorate --all

Quick check

Why is comparing only the final file content insufficient for this lesson?

The learner has compared the resulting files from two disposable repositories.Parent lists and object IDs reveal whether the branch adopted an existing commit or a newly created merge commit.

What `--no-commit` can and cannot pause

`--no-commit` can stop a true merge immediately before the merge commit is created, allowing inspection before the commit is recorded. It cannot pause a fast-forward because a fast-forward creates no merge commit to stop before. If the exercise requires stopping before a merge commit, `--no-ff --no-commit` makes the intended merge-commit path explicit.

bash
git merge --no-ff --no-commit topic
git status --short --branch
git rev-parse HEAD
git rev-parse MERGE_HEAD
git merge --abort

Quick check

Why can `--no-commit` not stop an ordinary fast-forward before a merge commit?

The named tip is a descendant of the current tip and default fast-forwarding is allowed.The branch update completes without creating a merge commit; `--no-commit` cannot create a pause that does not correspond to a commit step.

Boundary with the next lessons

This lesson owns the history-shape outcomes of a merge: fast-forward, true merge, `--no-ff`, and `--ff-only`. It does not teach conflict resolution, `MERGE_HEAD`, `git merge --continue`, `git merge --abort`, or detailed conflict states. Those belong to the later conflict and merge-state-machine lessons. It also does not re-teach merge-base calculation from V436.

Practice what you learned

Use the dedicated Git code-completion exercise for this lesson.

Official-source boundary

This lesson uses only official Git documentation: `git-merge`, `merge-config`, `git-merge-base`, and the official Git revision/glossary material needed for ancestry terminology. The current official documentation baseline is Git 2.56.0. The local verifier is Git 2.47.3; observed behavior is reported from that executable and is not silently generalized across versions.

Was this page helpful?

Mark this learning resource complete to track your learning progress.