Free Ebook cover Git for Programmers: Version Control for Small Projects

Git for Programmers: Version Control for Small Projects

New course

7 pages

Merging with Confidence: Fast-Forward, Merge Commits, and Conflict Resolution

Capítulo 5

Estimated reading time: 9 minutes

+ Exercise

Fast-forward merge vs. merge commit

What Git is doing when you merge

A merge combines the work from one branch into another. Git looks at the two branch tips and their common ancestor (the “merge base”) and decides whether it can simply move a branch pointer forward or whether it must create a new commit that ties two histories together.

Fast-forward merge (no new commit)

A fast-forward happens when the branch you are merging into has not diverged. In other words, the target branch tip is an ancestor of the source branch tip. Git can “fast-forward” by moving the target branch pointer to the newer commit.

  • Result: linear history; no merge commit is created.
  • Signal in history: it can look like the work was done directly on the target branch, because there is no explicit merge point.
# Example: main has not moved since you branched feature/login off it. Merge can fast-forward. git switch main git merge feature/login

Merge commit (explicit merge node)

A merge commit is created when both branches have new commits since they split (diverged). Git must combine two lines of history and records that combination in a new commit with two parents.

  • Result: non-linear history; a merge commit is created.
  • Signal in history: the merge commit marks the integration point, which can make later archaeology easier (e.g., “this feature landed here”).
# Example: both main and feature/login have new commits. Merge commit is required. git switch main git merge feature/login

When each is useful (and how to keep merges understandable)

When fast-forward merges are useful

  • Small, short-lived branches where you want a clean linear story.
  • Solo work where the branch is just a temporary workspace and you do not need an explicit integration marker.
  • Simple changes where the branch name is not important to preserve in history.

If you want to ensure a fast-forward only (and fail otherwise), use --ff-only. This is a safety feature: it prevents an unexpected merge commit and forces you to update your target branch first.

# Merge only if it can fast-forward; otherwise stop with an error. git switch main git merge --ff-only feature/login

When merge commits are useful

  • Preserving context: a merge commit can document “this set of commits is one integrated change.”
  • Multiple commits on a feature branch where you want to keep the branch boundary visible.
  • Busy main branch: if main moved while you worked, the merge commit reflects the real integration moment.

If you want to always create a merge commit (even if a fast-forward is possible), use --no-ff. This is often used when you want the merge to be visible in git log --graph.

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

# Force a merge commit even if fast-forward is possible. git switch main git merge --no-ff feature/login

Keeping merges understandable

  • Merge from a clean main: update main before merging so you are integrating into the latest state.
  • Use descriptive merge commit messages when a merge commit is created (Git will open an editor; keep the message meaningful).
  • Avoid “mega merges”: integrate smaller, focused branches rather than accumulating weeks of changes.
  • Verify before merging: run tests and check the diff so you know what you are integrating.

How to perform merges safely from main

Safe merge checklist (local workflow)

This sequence reduces surprises by ensuring you are on the correct branch, up to date, and able to inspect what will be merged.

# 1) Switch to main (the integration branch) git switch main # 2) Update main from the remote (if you use one) git pull --ff-only # 3) Inspect what will be merged (optional but recommended) git log --oneline --decorate --graph --all --max-count=20 # 4) Merge the feature branch into main git merge feature/login # 5) Run tests / build after the merge (project-specific) # e.g., npm test, pytest, mvn test, etc.

Previewing changes before you merge

To understand what the merge will bring in, compare branches directly. This helps you catch accidental changes (debug prints, generated files, unrelated edits) before integration.

# Show commits on feature not yet in main git log --oneline main..feature/login # Show file-level differences git diff --stat main..feature/login # Inspect the actual diff git diff main..feature/login

Conflict resolution

Why conflicts happen

A conflict occurs when Git cannot automatically combine changes. The most common case is two branches editing the same lines of the same file differently. Git can usually merge non-overlapping edits, but overlapping edits require a human decision.

  • Typical triggers: edits to the same function, renaming files while another branch edits them, large refactors combined with feature work.
  • Important point: a conflict is not an error in your code; it is Git asking you to choose the correct final content.

How to locate conflicts with git status

When a merge stops due to conflicts, Git leaves the repository in a “merge in progress” state. Your first command should be git status.

git switch main git merge feature/login # If conflicts occur: git status

git status will list unmerged paths and typically labels them as “both modified” (or similar). Those are the files you must resolve.

How to read conflict markers

Git writes conflict markers directly into the affected files. You will see a structure like this:

<<<<<<< HEAD // content from the branch you are merging into (currently checked out) function greet() { return "Hello from main"; } ======= // content from the branch you are merging from function greet() { return "Hello from feature"; } >>>>>>> feature/login
  • <<<<<<< HEAD: the version from your current branch (the target of the merge).
  • =======: separator between the two versions.
  • >>>>>>> branch-name: the version from the branch being merged in.

Your job is to edit the file into the correct final form, removing the markers and choosing (or combining) the content.

Resolving by editing files and completing the merge commit

A manual conflict resolution typically follows this loop: open the conflicted file, decide the final content, save it, stage it, and repeat until all conflicts are resolved.

# After editing a conflicted file to remove markers: git add path/to/conflicted-file # Repeat for all conflicted files, then finish the merge: git commit

During a merge conflict, git commit completes the merge by creating the merge commit (unless the merge can be fast-forwarded, which it cannot if you had conflicts). Git usually provides a default merge message; you can keep it or refine it.

If you realize you merged the wrong branch or the conflict is too messy, you can abort the merge and return to the pre-merge state:

# Abort the merge attempt and go back to the state before merging git merge --abort

Strategies to reduce conflicts

  • Pull/merge often: integrate changes frequently so you resolve small conflicts instead of large ones.
  • Keep changes small and focused: avoid mixing refactors, formatting, and feature changes in the same branch.
  • Avoid long-lived branches: the longer a branch lives, the more likely main diverges in the same areas.
  • Coordinate on “hot” files: if multiple changes touch the same core file, sequence the work or split responsibilities.
  • Prefer mechanical refactors separately: large renames/reformats create noisy diffs and increase conflict probability.

Guided conflict lab: create, merge, resolve, verify

Goal

You will deliberately create a conflict by editing the same line on two branches, attempt a merge, resolve the conflict, and verify the result with tests and git log --graph.

1) Create a small starting file on main

Start from a clean working tree. Create a file that is easy to conflict.

git switch main # Create a file (example uses a simple JS module) printf 'export function greeting() {\n  return "Hello";\n}\n' > app.js git add app.js git commit -m "Add greeting function"

2) Create branch A and change the same line

git switch -c feature/a # Edit the return line in app.js to something different # (Use your editor; example with sed for a single-line replacement) sed -i.bak 's/return "Hello";/return "Hello from A";/' app.js rm -f app.js.bak git add app.js git commit -m "Change greeting to A"

3) Create branch B from main and change the same line differently

Switch back to main, then create another branch so both branches diverge from the same base commit.

git switch main git switch -c feature/b sed -i.bak 's/return "Hello";/return "Hello from B";/' app.js rm -f app.js.bak git add app.js git commit -m "Change greeting to B"

4) Merge branch A into main (no conflict yet)

First integrate one branch. This should merge cleanly because main still has the original line.

git switch main git merge feature/a

5) Merge branch B into main (conflict expected)

Now main contains “Hello from A”, while feature/b contains “Hello from B”. Git cannot choose automatically.

git merge feature/b # Expect: CONFLICT (content): Merge conflict in app.js git status

6) Resolve the conflict by editing app.js

Open app.js and you should see conflict markers. Decide the final output. For the lab, choose a combined message such as “Hello from A and B”. Edit the file so it becomes valid code again and contains no markers.

# app.js after resolution (example final content) export function greeting() {   return "Hello from A and B"; }

Stage the resolved file and complete the merge commit.

git add app.js git status # should show all conflicts fixed, ready to commit git commit

7) Verify the final result

Verification should include both functional checks (tests) and history inspection.

# Run your project tests/build here (examples) # npm test # pytest # make test # Inspect the merge structure git log --graph --oneline --decorate --max-count=20

In the graph output, you should see a merge commit with two parents (the integration of feature/b after feature/a). Confirm that app.js contains your chosen final line and that your tests pass.

Now answer the exercise about the content:

During a merge, when will Git perform a fast-forward instead of creating a merge commit?

You are right! Congratulations, now go to the next page

You missed! Try again.

A fast-forward merge happens only when the target branch hasn’t diverged (its tip is an ancestor of the source), allowing Git to advance the branch pointer without creating a new merge commit.

Next chapter

Sharing Work: Remotes, Pull Requests, and Reviewing Changes with Git

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.