Merging combines changes from one branch into another. It is how feature work eventually becomes part of main or another shared branch.
A common example is merging feature/login into main after the feature is complete and reviewed.
git switch main
git merge feature/loginGit tries to combine history automatically. When histories fit together cleanly, merging is easy. When the same lines changed in different ways, you may get a conflict that needs manual resolution.
A fast-forward merge happens when the target branch has not moved since the feature branch was created. Git can simply move the branch pointer forward.
Imagine this sequence:
main at commit Afeature/docsfeature/docsmain still points at AMerging can look like this:
git switch main
git merge feature/docsGit may respond with a fast-forward update because there is no need to create a separate merge commit.
The histories form a straight line. main can just âcatch upâ to the feature branch.
Benefits:
A three-way merge happens when both branches moved forward independently.
For example:
main has commit Dfeature/login has commits B and CNow Git must combine two lines of development. It creates a new merge commit that has two parents.
git switch main
git merge feature/loginThis is called a three-way merge because Git compares:
--no-ffBy default, Git may fast-forward when possible. Some teams prefer to always keep a visible merge commit for feature branches.
Use:
git switch main
git merge --no-ff feature/loginThis forces a merge commit even if a fast-forward would work.
--no-ff?It can make history easier to read because the branch integration remains visible as a single merge event. That can help when reviewing past work or reverting an entire feature.
Tip: Teams that rely heavily on pull requests often like explicit merge commits because they preserve the story of how a feature entered the main branch.
A merge conflict happens when Git cannot decide how to combine changes automatically.
This usually occurs when:
Git will pause the merge and ask you to resolve the conflict manually.
Assume both branches edited the same line in app.conf.
Start the merge:
git switch main
git merge feature/config-updateGit may report a conflict. Check the status:
git statusOpen the conflicted file. You may see markers like this:
<<<<<<< HEAD
port=8080
=======
port=9090
>>>>>>> feature/config-update<<<<<<< HEAD shows the version from the current branch======= separates the two versions>>>>>>> feature/config-update shows the incoming versionChoose the correct final content, for example:
port=9090Or combine both ideas if needed. Then stage the resolved file:
git add app.confComplete the merge:
git commitGit usually opens your editor with a default merge message.
After merging, inspect the history:
git log --oneline --graph --decorate -10This helps you confirm whether Git created a fast-forward result or a merge commit.
You can also test the application after resolving conflicts:
npm testOr if it is a shell-based project:
./run-checks.shValidation matters because a âcleanâ merge is not automatically a correct merge.
If you start a merge and realize you are not ready, you can often stop and return to the pre-merge state:
git merge --abortThis is helpful if you accidentally merged the wrong branch or want to review changes before trying again.
Note:
git merge --abortworks best before you make extra manual changes beyond the merge conflict state.
Merging preserves the true branch structure. Rebasing rewrites commits onto a new base to create a straighter history. Both are useful, but merge is often the safer beginner choice because it does not rewrite existing commits.
Merging is how Git combines branches. A fast-forward merge simply moves a pointer. A three-way merge creates a merge commit when histories diverged. Conflicts happen when Git cannot safely decide between competing changes, and resolving them means editing the affected files, staging the results, and finishing the merge.
Use --no-ff when your team wants feature integrations to remain clearly visible in history.
Scenario: main is still at commit A, while feature/docs moved from A to B to C and nothing new was added to main.
Question: What kind of merge should Git usually be able to perform when you merge feature/docs into main?
<MCQExercise
exercise={{
id: 'git-merging-q1',
title: 'Exercise 1: Recognizing a fast-forward',
description: 'Identify the merge type when the target branch has not moved since branching.',
options: [
{ id: 'a', text: 'A detached HEAD merge, because main is too old to merge.' },
{ id: 'b', text: 'A conflict-only merge, because every feature branch must create a conflict.' },
{ id: 'c', text: 'A fast-forward merge, because Git can move the main pointer straight to C.' },
{ id: 'd', text: 'A revert merge, because main must undo A before accepting B and C.' },
],
correctId: 'c',
explanation: 'The lesson defined a fast-forward merge as the case where the target branch has not advanced independently. Git can simply move main forward to the feature tip because the history is still a straight line.',
wrongExplanations: {
a: 'Detached HEAD is about checkout state, not this merge shape.',
b: 'Conflicts happen only when Git cannot combine competing changes automatically.',
d: 'Revert is for undoing commits, not for normal branch integration.',
},
}}
/>
Question: Why might a team choose git merge --no-ff feature/login even when a fast-forward merge is possible?
<MCQExercise
exercise={{
id: 'git-merging-q2',
title: 'Exercise 2: Preserving feature history',
description: 'Pick the reason teams force a merge commit with --no-ff.',
options: [
{ id: 'a', text: 'It skips code review and merges more quickly.' },
{ id: 'b', text: 'It keeps a visible merge commit so the feature integration remains clear in history.' },
{ id: 'c', text: 'It prevents the branch from ever being deleted.' },
{ id: 'd', text: 'It rewrites the feature branch into a linear rebase automatically.' },
],
correctId: 'b',
explanation: 'The tutorial explained that --no-ff forces a merge commit so the feature arrival stays visible as a single event. Teams often like that because it makes reviews, audits, and full-feature reverts easier to understand later.',
wrongExplanations: {
a: 'Merge strategy does not eliminate the need for review.',
c: 'Branch deletion is a separate cleanup decision after the merge.',
d: 'Rebase and merge are different workflows. --no-ff does not transform a merge into a rebase.',
},
}}
/>
Scenario: A merge stopped with conflict markers in app.conf. You edited the file so the final content is correct.
Question: According to the tutorial, what should you do next to complete the merge workflow?
<MCQExercise
exercise={{
id: 'git-merging-q3',
title: 'Exercise 3: Finishing conflict resolution',
description: 'Choose the next step after manually resolving merge conflict markers.',
options: [
{ id: 'a', text: 'Run git clone to create a clean copy before saving the merge.' },
{ id: 'b', text: 'Delete the incoming branch immediately so Git assumes the conflict is fixed.' },
{ id: 'c', text: 'Run git merge --abort even though the file is already resolved.' },
{ id: 'd', text: 'Stage the resolved file with git add app.conf, then complete the merge commit.' },
],
correctId: 'd',
explanation: 'The lesson showed that after you resolve the conflict markers, you stage the corrected file so Git knows the conflict is handled. Then you complete the merge, typically with git commit if Git has not already created the final commit message step for you.',
wrongExplanations: {
a: 'Cloning again does not mark the conflict as resolved in the current repository.',
b: 'Deleting a branch does not resolve the merge state or record the final content.',
c: 'Abort is for cancelling the merge, not for finishing a conflict you already resolved.',
},
}}
/>
A fast-forward happens when the target branch has no commits since you branched off, so Git simply moves the pointer forward to your latest commit, no merge commit, and history stays linear. A three-way merge happens when both branches have moved on, so Git combines them using the common ancestor plus each tip, and records the result as a merge commit with two parents. You cannot choose: Git fast-forwards whenever it can, unless you pass --no-ff to force a merge commit.
When you want the branch to remain visible in history. --no-ff forces a merge commit even where a fast-forward was possible, so the graph shows that a group of commits belonged to one feature and git log --graph reads as a series of merged branches. That makes reverting a whole feature easy, one git revert -m 1 on the merge commit, and it is why many teams enable it for release branches. The cost is a busier graph, which is why other teams prefer squash merges instead.
git merge --abort returns you to the state before the merge began, discarding partial resolutions. It works while the merge is in progress, that is, before you commit, which covers the case where the conflicts turn out to be more than you expected. If you have already committed the merge, use git reset --hard HEAD~1 to drop it locally, and git revert -m 1 <merge-sha> if it has already been pushed, since resetting a shared branch strands everyone else.
Every commit on the branch you are merging is already reachable from your current branch, so there is nothing to bring in. The usual cause is not having fetched: your local copy of the remote branch is stale, and Git is telling you the truth about what it knows. Run git fetch first, then merge. The other cause is merging the wrong direction. Merging main into your feature branch when you meant to merge your feature into main.
Squash merges suit most teams: each pull request becomes one commit on main, so history reads as a list of shipped changes and reverting is trivial, at the cost of losing the individual commits. Merge commits preserve everything and keep the branch visible, which matters when you need the full record for audit or bisecting inside a feature. Rebase merges give a linear history with every commit kept, which is tidy but rewrites hashes. Pick one and enforce it in the repository settings rather than leaving it per-developer.
Use git rebase for a linear history: basic and interactive rebase, squashing, recovering with --abort and reflog, force-with-lease, and when to merge instead.
Work with Git remotes confidently: fetch versus pull, fixing a rejected push without force, upstream tracking, pruning branches and origin versus upstream.
Park work in progress with git stash: apply versus pop, stash untracked files, name and inspect stashes, and decide when a branch is the better tool instead.