A branch only has value once its work returns to the main timeline. Merging is how that happens. Most merges are silent — Git figures out how to combine the changes and you don't even notice. The interesting cases are merge conflicts, where two branches changed the same line of the same file in different ways and Git can't decide which version to keep. This lesson covers both flavours, with a real test-config conflict you walk through end to end.
What git merge actually does
Merging takes the commits from one branch and integrates them into another. The mental model: you're standing on the destination branch (usually main) and pulling the source branch's work into you.
The two-step pattern:
git switch main # the branch you want to merge INTO
git merge feature/login-tests # the branch you want to merge FROMThe order matters. You merge into the branch you're currently on, from the branch you name. A common beginner error is doing it backwards — switching to the feature branch and then git merge main, which merges main's changes into the feature branch, not the other way round.
Fast-forward merge
The simplest case: main hasn't moved since you branched. Git just slides the main pointer forward to wherever your feature branch is. No new commit is needed.
git switch main
git merge feature/add-login-testUpdating 4c48901..7a92f1e
Fast-forward
tests/login.spec.js | 12 ++++++++++++
1 file changed, 12 insertions(+)
"Fast-forward" means: same line of history, just longer. Both branches now point at the same commit. Clean, fast, no merge commit clutter.
Three-way merge
The more common case: while your feature branch was growing, someone else also pushed work to main. Now both branches have new commits since they diverged. Git creates a merge commit that combines both lines.
git switch main
git pull # get teammates' new commits
git merge feature/search-testsMerge made by the 'ort' strategy.
tests/search.spec.js | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
A new commit appears in git log with two parents — one from main, one from the feature branch. Both histories are preserved.
When merging hits a conflict
A conflict happens when the same line of the same file was changed differently in both branches. Git can't pick a winner. It pauses the merge and asks you to decide.
A real QA scenario: you and a developer both edited cypress.config.js. You changed the timeout from 5000 to 10000 because the staging environment got slower. The developer changed it from 5000 to 15000 because they wanted more headroom for a new e2e test. Both of you committed; both of you pushed.
When you try to merge their branch into yours:
git switch main
git merge feature/longer-timeoutsAuto-merging cypress.config.js
CONFLICT (content): Merge conflict in cypress.config.js
Automatic merge failed; fix conflicts and then commit the result.
git status confirms what's wrong:
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: cypress.config.js
What a conflict looks like inside the file
Open cypress.config.js:
module.exports = {
e2e: {
baseUrl: 'https://staging.acme.com',
<<<<<<< HEAD
defaultCommandTimeout: 10000,
=======
defaultCommandTimeout: 15000,
>>>>>>> feature/longer-timeouts
video: false
}
};Three markers split the conflict region:
<<<<<<< HEAD— start of the version on the branch you're merging into (in this casemain, your version).=======— divider.>>>>>>> feature/longer-timeouts— end of the version coming from the other branch.
The text between <<<<<<< and ======= is "yours" (HEAD). The text between ======= and >>>>>>> is "theirs" (incoming). Your job is to edit the file to the version you actually want, removing the markers entirely.
Resolving the conflict
You decide 15000 is the right value (the dev's reasoning was sound). Edit the file to:
module.exports = {
e2e: {
baseUrl: 'https://staging.acme.com',
defaultCommandTimeout: 15000,
video: false
}
};All three markers gone. One value left. Save the file.
Tell Git you're done with this file:
git add cypress.config.js
git statusOn branch main
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: cypress.config.js
Finalise with a commit. Git pre-fills a sensible message:
git commit[main 9f2e3b4] Merge branch 'feature/longer-timeouts'
The merge is complete. Both branches' histories are preserved in the log; the conflicting line is now a single agreed value.
VS Code's merge editor
Resolving conflicts by hand is fine, but VS Code makes it visual. Open the conflicted file and you'll see inline buttons:
- Accept Current Change — keep your version (HEAD).
- Accept Incoming Change — take their version.
- Accept Both Changes — keep both (rare; useful if both blocks add new code that doesn't overlap logically).
- Compare Changes — opens a side-by-side three-pane editor with current, incoming, and the resulting merge.
Click the right option, save, then git add and git commit. For non-trivial conflicts the three-pane editor is dramatically faster than hand-editing.
Clean merge vs conflicted merge
Two flavours of merge — what you'll see and what to do
Clean merge
git merge feature/foo
Auto-merging files... Merge made by the 'ort' strategy.
Files don't overlap
Both branches changed different lines or different files
No action needed
Git creates the merge commit automatically (or fast-forwards)
Push and you're done
git push
What you want most of the time
Conflicted merge
CONFLICT (content): Merge conflict in...
Same line, two versions, Git can't choose
Markers in the file
<<<<<<< HEAD / ======= / >>>>>>> branch-name
Edit the file by hand
Remove markers, keep the right text — or accept-current/accept-incoming in VS Code
git add then git commit
Stage the resolved file and finalise the merge
Stop, slow down, get it right
Bailing out of a merge
If a conflict is messier than expected and you want to abandon the merge entirely:
git merge --abortThe repo returns to exactly its pre-merge state. Useful when you realise you're merging the wrong branch or want to rebase first.
Preventing conflicts in the first place
Conflicts are a function of two things: how long branches diverge and how much overlap their changes have. You can't always avoid the second, but the first is in your control:
- Pull main into your branch frequently. Daily is good.
git switch your-branch && git merge mainbrings in everyone's latest work in small chunks rather than one giant chunk at the end. - Keep branches short-lived. A two-day branch usually merges clean. A two-week branch usually doesn't.
- Communicate. "I'm refactoring
cypress.config.jstoday" in standup gives everyone the chance to stage their changes around it.
⚠️ Common mistakes
- Committing the conflict markers. Beginners sometimes save the file with
<<<<<<<still inside, rungit add, and commit. The code now has literal merge-marker text in production. Always search for<<<<<<<before committing —grep -r "<<<<<<<" .flags any leftovers. - Resolving by deleting the file. When two people both edited a file and you don't understand it, the temptation is to nuke it and "start over." That throws away everyone's work. Read both versions carefully; pick the right merge.
- Pushing without testing the merged result. A clean text-level merge can still produce a broken project — the merged config might be syntactically valid but logically wrong. Always run the test suite after a non-trivial merge before you push.
🎯 Practice task
Manufacture a conflict and resolve it. 25-30 minutes.
- In your
qa-sandboxrepo, on main, createconfig.jswith one line:const timeout = 5000;. Commit and push (or just commit if you're staying local). - Branch off:
git switch -c feature/longer-timeout. Edit the line toconst timeout = 10000;. Commit. - Switch back:
git switch main. Edit the same line toconst timeout = 7500;. Commit. - Merge:
git merge feature/longer-timeout. Read the CONFLICT message. - Open
config.jsand observe the<<<<<<</=======/>>>>>>>markers. Choose one value, delete all markers, save the file. - Run
git add config.js && git commit. Git opens an editor with a default merge message — accept it. - Run
git log --oneline --graph --all. Notice the visible branch-and-merge shape in the history. - Stretch: repeat from step 2, but this time use VS Code's merge editor (open the file, click "Resolve in Merge Editor"). Compare the experience to the manual approach.
The next lesson zooms out from the mechanics to the strategy: which branching pattern your team should follow and how it shapes everything else.