Two paths for integrating branches

Branches are where Git development happens — separate containers for separate lines of work. The real question comes when work on one branch needs to get into another. Git offers two commands for that job, git merge and git rebase, and they solve the same problem: integrating changes from one branch into another. They just take very different routes to get there.

How merges combine history

A merge integrates the current state of one branch into the branch you're on. Say branch-B has new commits you want in branch-A. The command is straightforward:

$ git checkout branch-A
$ git merge branch-B

Git performs this by looking at three commits. First, it finds the common ancestor — the point where both branches shared identical content before diverging. Then it looks at the endpoints, the latest commits on each branch. Combining those three snapshots lets Git produce a true integration of both lines of development.

One simplified case exists where the integration is trivial: if the branch you're merging into hasn't moved since the common ancestor, Git can simply add the other branch's commits right on top. This is a "fast-forward" merge — both branches end up pointing at the same history, and no special commit is needed.

The more common scenario, where both branches have advanced independently, requires Git to create a new commit that carries all the combined changes. That's the merge commit.

Merge commits aren't human commits

A regular commit is a carefully constructed unit: a human collects related changes and writes a meaningful message. A merge commit is different on both counts. Git creates it automatically, and its purpose isn't to document a logical set of changes — it's to tie two branch histories back together.

To understand what an automatic merge actually did, you have to examine the commit history of both branches, not just the single merge commit itself.

Rebase: a different kind of integration

A rebase isn't better or worse than a merge — it's simply a different approach. Some developers don't like automatic merge commits and prefer a history that reads as a straight line, as if the work was never split across branches at all. A rebase delivers that.

The starting scenario looks the same as the merge example. To integrate branch-B into branch-A via rebase, the command is equally short:

$ git checkout branch-A
$ git rebase branch-B

Behind the scenes, the operation happens in three steps. First, Git removes the commits on branch-A that came after the common ancestor — not destroying them, but parking them somewhere safe. Then it applies the new commits from branch-B, leaving both branches temporarily identical. Finally, those parked commits are placed on top of the integrated commits from branch-B — effectively, they are rebased.

The result: a project history that looks like a single, uninterrupted line of development. No merge commit, and the original commit structure is preserved.

Rewriting history has a cost

The key caveat with git rebase is that it rewrites the commit history. Look closely at the final state: commit C3* contains the same changes as the original C3, but it is literally a different commit. Before the rebase, its parent was C1; afterward, its parent is C4. A commit's identity rests on a few properties — author, date, changeset, parent — and altering any of them generates a completely new commit with a new SHA-1 hash.

That's a non-issue for commits that have never been published. It becomes a genuine problem when you rewrite commits already pushed to a remote. If another developer based work on the original C3, that commit no longer exists after your rebase.

The safe rule is simple: never rebase commits already pushed to a shared remote. Use git rebase to clean up your own local history before integrating it into a team branch. Merging is the non-destructive option that preserves existing history exactly as it happened. Rebasing offers a tidier, linear narrative — when used locally.