A Quick Primer

Staying in sync: fetch, fast-forward, and fixing conflicts

How to pull down someone else's changes safely, why some updates are effortless and others need a real merge, and exactly how to resolve a conflict when two branches edit the same lines. The thread running through all of it: fetch first, inspect before you change anything, then only act when the inspection proves it's safe.

01 Fetch & inspect

Learn what's out there before you touch your own work.

git fetch origin downloads the latest state of the remote — new commits and branches — into your local copy without changing any of your working files. It updates the origin/* "remote-tracking" references: think of them as a local snapshot of what the remote looks like right now. This is always the safe first move.

Then git status tells you where you stand relative to the remote: behind (the remote has commits you don't), ahead (you have local commits not yet pushed), or diverged (both sides have unique commits — the tricky case). For an exact count:

git rev-list --left-right --count HEAD...origin/main
# prints two numbers: <your-unique>  <remote-unique>
# e.g. "0    2"  →  you have 0 unique, remote has 2

That 0 on the left is the magic number — it means your side has nothing unique, so the update can't possibly conflict.

The single most useful trick here is the two-dot range: git log A..B lists "commits in B but not in A." To ask "is it safe to delete this branch?", run git log origin/main..origin/some-branch — empty output means every commit is already in main, so nothing is lost.

02 The easy case: fast-forward

No merge needed — just slide the pointer forward.

When your branch has zero unique commits and the remote is ahead, there's nothing to actually combine. Git can just move your branch pointer forward to match the remote. This is a fast-forward:

git merge --ff-only origin/main

The --ff-only flag is a safety rail: "only do this if it's a clean fast-forward; refuse and stop if a real merge would be needed." That way you never silently create a merge commit or trigger a conflict. (Plain git pull works too, but --ff-only makes the safety explicit.) Visually, your pointer just slides from C to E:

before:   A---B---C            (you)
                   \
                    D---E       (origin/main)

after:    A---B---C---D---E      (you = origin/main)

03 The hard case: diverged history

Both sides built on the same commit, then went separate ways.

When git status says "have diverged, and have N and M different commits each," there's no pointer to just slide — Git has to genuinely combine two lines of history:

            D---E       (your local main)
           /
A---B---C
           \
            F---G       (origin/main)

One key distinction first: diverging is not the same as conflicting. If the two sides touched different files (or different parts of the same file), Git combines them automatically. A real conflict only happens when both sides changed the same lines differently — then Git can't guess which you want, so it stops and asks. You have two strategies to reconcile:

Option A — Merge (git merge origin/main)

Creates a new merge commit that ties both histories together. Preserves exactly what happened on both sides; history shows the fork. The default, and the right choice when others share the branch.

A---B---C---D---E---M
             \     /
              F---G

Option B — Rebase (git rebase origin/main)

Replays your commits on top of the remote's, as if you'd started from the latest. Linear history, no merge commit — cleaner. But it rewrites your commits (DD', new hashes), so the rule is: only rebase commits you haven't pushed/shared yet.

A---B---C---F---G---D'---E'
Rule of thumb: merge to preserve a shared, public history; rebase to keep your own unpushed work tidy and linear. For a solo project, rebase is usually the cleaner pick.

04 Resolving an actual conflict

Git pauses and hands you the file. You decide what's correct.

When either strategy hits the same lines, Git stops and git status shows the file as "both modified." Open it and you'll see conflict markers:

<<<<<<< HEAD
  color: #F5A623;   /* your version */
=======
  color: #E8930C;   /* incoming from origin/main */
>>>>>>> origin/main

You fix it by hand: edit the file so it reads correctly — keep one side, keep the other, or blend them — and delete all three marker lines. The goal is a clean file, as if you'd written it correctly the first time. Then mark it resolved and finish:

git add path/to/file       # mark this file's conflict resolved
# ...repeat for each conflicted file...
git commit                 # if merging — finalizes the merge commit
# ── OR ──
git rebase --continue      # if rebasing — on to your next replayed commit

For big or many conflicts, lean on tooling: git mergetool opens a visual 3-way diff (yours / theirs / common base), and editors like VS Code put clickable Accept Current / Accept Incoming / Accept Both buttons right on the markers. If you know you want one side wholesale, git checkout --ours path or --theirs path resolves a whole file in one shot.

05 The escape hatch

Nothing is final until you say so — you can always back out.

If a merge or rebase gets messy and you'd rather return to exactly where you started, bail out with no harm done:

git merge --abort      # during a merge
git rebase --abort     # during a rebase

This is why the fetch-then-inspect habit is so safe: nothing is committed until you resolve and confirm, and until then you can always abort back to the starting point. The same range trick that tells you a branch is safe to delete (A..B) is the one that tells you what a pull or a PR actually adds — learn it once, reuse it everywhere.

The mental model

Fetch → inspect → act only when it's provably safe → clean up.

  • Fast-forward = no merge needed, just slide the pointer.
  • Diverged = real merge needed → pick merge or rebase → resolve same-line conflicts by editing the file → add + continue/commit, or --abort to back out.