The day Git taught me the difference between merge and rebase | KMS ITC | KMS ITC - Your Trusted IT Consulting Partner
KMS ITC
Engineering 9 min read

The day Git taught me the difference between merge and rebase

A deep dive into accidental merges, silent deletions, and why Git’s three-way merge can ‘correctly’ make your files disappear.

KI

KMS ITC

#git #devops #engineering #version-control #incident-learning

There are Git errors that shout at you.

And there are Git mistakes that stay completely silent.

The silent ones are the dangerous ones.

This is the story of one of them — and the mental model shift that finally made it predictable.

Merge vs rebase: why deletion can win

Act I — the accident

I had a feature branch:

  • job-portal

From it, I branched:

  • job-portal-2

Meanwhile, an unfinished version of job-portal was accidentally merged into master.

Production wasn’t supposed to include that feature.

By the time I realized it, other commits had already landed on master. Reverting the merge cleanly would have been messy (and high-risk).

So I made a practical decision:

  1. Manually delete the unintended files
  2. Commit the deletions
  3. Push to master
  4. Restore production to its correct state

At that point:

  • master was correct
  • but the Git history had changed in a subtle way

And that subtlety would matter.

Act II — the mystery of the missing files

Later, I wanted job-portal-2 to contain:

  • latest master
  • plus the full job-portal feature

So I merged master into job-portal-2.

No conflicts.

Everything looked fine.

Except something wasn’t fine.

The job-portal files were gone. Completely.

No errors. No warnings. Just… absent.

I then tried:

  • git merge job-portal

Still nothing came back.

At this point, it felt like Git was ignoring me.

It wasn’t.

Git was being perfectly logical.

Act III — the merge algorithm (and why deletion “wins”)

Here is the critical principle:

Git does not compare branches by current file state.

Git compares changes since the common ancestor.

That’s three-way merge logic.

Let’s simplify the history.

Before the accident

  • job-portal adds files
  • job-portal-2 branches off job-portal

After the accidental merge + manual cleanup

  • master contains a merge commit that introduced the feature
  • then a later commit deletes those feature files

Now the important part:

When I merged master into job-portal-2, Git looked for:

  • common_ancestor(master, job-portal-2)

That ancestor was before the deletion.

So Git compared:

  • ancestor → master: files deleted
  • ancestor → job-portal-2: no changes to those files

Git concluded:

  • the deletion happened
  • the feature branch didn’t modify those files after the fork
  • therefore, deletion wins

This is correct three-way merge behavior.

Why merging job-portal didn’t resurrect the files

Because from Git’s perspective:

  • the files already existed at the fork point
  • they were deleted later in master
  • the feature branch never modified them after that point

So Git saw no “new change” to apply.

There is no concept of “restore what’s different right now.”

Git reasons in terms of change, not state.

Once you accept that, the behavior stops being mysterious.

Act IV — why rebase works when merge doesn’t

Merge compares histories.

Rebase rewrites history.

That’s the key distinction.

When I ran:

git checkout job-portal
git rebase master

Git performed these steps:

  1. found the fork point
  2. temporarily removed feature commits
  3. moved the branch pointer to the latest master
  4. replayed feature commits one by one

Visually:

Before rebase

  • master: … → (delete feature files)
  • job-portal: feature commits that originally added those files

After rebase

  • job-portal commits are now applied after the deletion

That subtle change is everything.

Commits that originally “added a file” are now being replayed onto a branch where that file does not exist — so Git treats them as fresh additions.

The files came back.

Not because I forced them.

But because I replayed intent.

Merge vs rebase — a systems view

Here’s the operational mental model that actually holds up under pressure:

  • Merge asks: “What changed in each branch since the fork?”
  • Rebase asks: “What if this feature was written today on top of that branch?”

So:

  • Merge preserves historical truth (including deletions).
  • Rebase preserves developmental intent (by replaying commits on a new base).

In production recovery scenarios, intent often matters more — but it comes with one big cost: you’re rewriting history.

Practical guidance (when to use what)

Use merge when you want:

  • maximum auditability (no rewritten history)
  • shared branches with many collaborators
  • to preserve “what really happened” in the DAG

Use rebase when you want:

  • a feature branch that behaves as if it was written on top of the latest main
  • to resolve confusing ancestry (like silent deletion outcomes)
  • a clean, linear review story (especially before opening a PR)

Act V — the hidden commit (interactive rebase as hygiene)

After the rebase succeeded, I inspected the branch history and noticed something odd:

  • an unrelated “MFA implementation” commit

It belonged to another unfinished branch.

Rebase had faithfully replayed everything in my branch — including what I didn’t want.

So I cleaned it up with interactive rebase:

git rebase -i master

In the editor, I changed that unrelated commit from pick to drop.

Then Git replayed only the commits that actually belonged.

This is feature branch hygiene at its best:

  • not cosmetic
  • not “nice to have”
  • an architectural asset for preventing future confusion

Act VI — the force push discipline

One final subtlety:

Rebase rewrites commit hashes.

That means your local branch no longer matches the remote.

So clicking “Sync” in an IDE can be dangerous.

Use:

git push --force-with-lease

Not plain --force.

And avoid blind pull/sync after rewriting history.

Rebase demands deliberate pushes.

Engineering lessons extracted

This wasn’t just a Git problem. It revealed deeper discipline:

  1. Production is a source of truth

    • If master changes, feature branches must adapt.
  2. Merge is conservative

    • It respects historical deletion.
  3. Rebase is declarative

    • It says: “Apply my feature as if written now.”
  4. Silent success is not correctness

    • If Git does nothing, it may be doing exactly what you told it.
  5. Clean history is an architectural asset

    • Interactive rebase prevents future ambiguity.

The deeper reflection

Git is not about files.

It is about change graphs.

Once you understand that Git reasons over commit DAGs — not directories — everything becomes predictable.

The real shift happened when I stopped asking:

“Why doesn’t Git see my files?”

And started asking:

“What does Git think changed?”

That question changed everything.


If you’d like, reach out via /contact and we’ll share a lightweight “Git operating model” we use for production teams: branching rules, rebase/merge policy, and the minimum checks that prevent silent failure modes like this one.