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.
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.
git switch main
git merge topicQuick 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.
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 mainQuick 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.
git switch main
git merge --no-edit topic
git rev-list --parents -n 1 HEAD
git show --no-patch --format=fuller HEADQuick 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.
git switch main
git merge --no-ff --no-edit topic
git rev-list --parents -n 1 HEADQuick 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.
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.
git rev-parse HEAD
git rev-list --parents -n 1 HEAD
git log --graph --oneline --decorate --allQuick 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.
git merge --no-ff --no-commit topic
git status --short --branch
git rev-parse HEAD
git rev-parse MERGE_HEAD
git merge --abortQuick 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.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.