Git Merge
Predict when Git can fast-forward, when it creates a merge commit, and how the resulting graph should be read.
Integration changes reachability
Merge direction starts with the current branch
git merge feature/search means “integrate feature/search into the branch currently checked out.” The current branch is the destination ref; the named branch is an input. Reversing checkout direction can produce a different branch movement and first-parent story.
HEAD → main
Main is the branch Git may move. Its current tip becomes the first parent if a merge commit is created.
feature/search
The named tip contributes commits and tree changes. The feature ref itself does not move because of this merge.
The merge base explains divergence
A merge base is a best common ancestor of two commits. It anchors feature-side comparison and tells you whether one tip is already an ancestor of the other.
git status
git branch --show-current
git log --oneline --decorate --graph --all
git merge-base main feature/search
git log main..feature/search --oneline
git log feature/search..main --oneline
git diff main...feature/searchBase equals main
Main’s tip is an ancestor of feature. A fast-forward is possible.
Base differs from both tips
Both sides have unique commits. Histories diverged and a normal merge needs a merge commit if it succeeds.
Base equals feature
Feature is already an ancestor of main. There may be nothing new to integrate.
No single assumption
Criss-cross histories can have multiple merge bases. Ordinary beginner graphs usually have one, but commands—not drawings from memory—decide.
Test what you learned
Type the command that prints a best common ancestor of main and feature/search.
Fast-forward moves a ref to an existing commit
If main has not advanced since feature branched, the feature tip is a descendant of main. Git can integrate by moving main directly to the feature tip. No new commit object is needed.
BEFORE · featureF2Add search empty state
featureF1Add catalog search
BEFORE · mainBShared base; main can move to F2.
git switch main
git merge --ff-only feature/search
git log --oneline --decorate --graph --all
git diff main...feature/search
git statusAfterward, main and feature/search both point to F2. The feature commits retain their IDs and parent relationships. The graph has no explicit integration commit.
Diverged histories require integration
If main and feature both contain commits after their merge base, neither tip can simply replace the other without losing reachability from the destination branch. A successful normal merge creates a new snapshot with both tips as parents.
B → M1
Main added release configuration after the branch point.
B → F1 → F2
Feature added search behavior from the same base.
The merge result does not concatenate commits or replay feature commits. Git performs a three-way tree merge using the merge base, current tip, and other tip, then records the integrated snapshot in a new commit if no unresolved conflict remains.
A merge commit records multiple parents
git switch main
git merge --no-ff feature/search
git show --no-patch --pretty=raw HEAD
git show --stat --first-parent HEAD
git log --oneline --decorate --graph --allFormer main tip
The branch that was checked out when merge ran. First-parent history often reads as the integration timeline.
Feature tip
The named branch tip integrated by the merge. Its complete ancestry remains reachable.
The merge commit stores a full project snapshot, metadata, message, and parent IDs. Its patch is comparison-dependent because there is more than one parent. Use git show --first-parent, git show -m, or combined diff views only after deciding which integration question you are asking.
Force an explicit merge event with no-ff
git merge --no-ff feature/search creates a merge commit even when main could fast-forward. Teams may choose this to preserve a visible feature boundary and a first-parent integration event.
Benefit
A named merge commit can group a feature’s commits and record when the branch entered the destination line.
Cost
More merge commits add graph structure and messages that must remain meaningful.
Not a correctness guarantee
--no-ff changes topology. It does not improve code, tests, review, or conflict resolution by itself.
Use project policy
Choose fast-forward, no-ff, squash, or review-platform strategies consistently rather than ad hoc aesthetics.
“Already up to date” is an ancestry result
If every commit reachable from the named branch is already reachable from the current branch, merge has no new history to integrate. Git reports the branch as up to date.
git merge-base --is-ancestor feature/search main
echo $?
# Exit 0 means feature/search is an ancestor of main.
git log main..feature/search --oneline
# Empty output confirms no feature-only reachable commits.Shell exit-code display differs across shells; do not copy echo $? unchanged into PowerShell. The ancestry command itself is portable Git behavior.
Merge from a controlled working state
Begin with a clean index and working tree unless the project explicitly documents another workflow. Local changes can make the result harder to attribute or cause Git to refuse checkout updates.
- 01Confirm repository and destination
Print the top-level path, current branch, and HEAD ID.
- 02Prove clean state
Run status, plain diff, and cached diff. Account separately for untracked and ignored paths.
- 03Inspect ancestry and delta
Read the graph, merge base, unique commit ranges, and three-dot diff.
- 04Choose topology policy
Use normal merge,
--ff-only, or--no-ffbecause the intended graph is explicit. - 05Integrate and verify
Inspect parents and snapshot, run tests, and prove final reachability.
If merge stops, do not improvise
A merge can stop because changes overlap, local state blocks an update, a hook fails, or another condition needs attention. Read the exact message and status. The next lesson covers conflict resolution in depth.
git status
git diff
git diff --cached
# If you should return to the pre-merge state:
git merge --abortSquash is not a merge commit
git merge --squash feature/search prepares the net feature change in the index and working tree but does not create a merge commit, move HEAD, or record the feature tip as a parent. A later ordinary commit has one parent.
Preserves ancestry
Main’s history reaches the feature commits through the merge parent relationship.
Preserves net content only
Main receives a new single-parent commit with the combined patch; the feature commits are not ancestors of main.
Squash can fit a project’s review policy, but do not describe its result as “feature commits were merged” when the graph does not contain them.
Verify more than a successful command
git status
git log --oneline --decorate --graph --all
git branch --contains feature/search
git merge-base --is-ancestor feature/search main
git diff main...feature/search
# Run the repository's relevant tests, build, lint, or manual checks.After a true merge or fast-forward, feature should be an ancestor of main and the three-dot diff is usually empty because their merge base is the feature tip. Verification must also cover behavior: a graph can integrate cleanly while the resulting application is wrong.
Delete the branch only after proof
Once integration, tests, status, and containment are correct, git branch -d feature/search removes the local feature name. It does not delete commits that remain reachable from main.
git branch --merged main
git log main..feature/search --oneline
git branch -d feature/search
git log --oneline --decorate --graph --allGuided practice: produce both graph shapes
Use a disposable repository and begin with three commits on main.
- 01Create a fast-forward case
Branch from main, add two feature commits, and prove main has no unique commits.
- 02Integrate with ff-only
Predict the ref movement, merge, and prove no new commit object was created.
- 03Create divergence
Make one new commit on main and two on another feature branch from the prior base.
- 04Create a merge commit
Inspect both ranges and the feature patch, merge with no-ff, and identify both parent IDs.
- 05Verify and clean up
Run checks, prove containment, delete both merged branch names with lowercase
-d, and redraw the graph.
Independent lab: enforce and explain merge policy
Create a disposable repository that demonstrates all three policy outcomes:
- A feature integrated with
--ff-only. - A diverged feature where
--ff-onlyrefuses and leaves history unchanged. - The same diverged feature integrated with an ordinary merge commit.
- A linear feature integrated with
--no-ffto preserve an explicit integration event. - A squash integration on a separate practice branch, with proof that feature commits are not ancestors of the destination.
- For each outcome, record merge base, directional ranges, graph before and after, parent count, resulting tree, and relevant tests.
- Finish with clean status and a written recommendation for one policy, including its review and history trade-offs.
Common merge mistakes
Merging from the wrong branch
The current branch is the destination. Print it immediately before integration.
Assuming merge always creates a commit
Fast-forward moves a ref. Inspect ancestry and use an explicit policy option.
Reviewing only commit subjects
Inspect the three-dot patch and run behavioral checks before integration.
Calling squash a true merge
Squash transfers net content without parent ancestry from the feature branch.
Deleting before verification
Keep the feature ref until history, snapshot, tests, and containment all pass.
Equating no conflict with correctness
Git can combine text cleanly while producing invalid behavior. Test the integrated tree.
Lesson review
You can now predict integration from ancestry. Fast-forward moves the destination ref to an existing descendant; diverged histories need a merge commit to preserve both parent lines; policy flags make expected topology explicit; verification proves both reachability and behavior.
- I identify the destination branch and merge base before merging.
- I distinguish ref movement, true merge commits, and squash content transfer.
- I use
--ff-onlyor--no-ffwhen topology is part of the contract. - I can name the first and second parents of a two-branch merge commit.
- I verify the graph, resulting snapshot, tests, and containment before branch deletion.