Git’s merge and rebase are two fundamental strategies for integrating changes from one branch into another, and picking the right one is crucial for maintaining a clean and understandable project history.

Let’s see them in action. Imagine you’re working on a feature in a branch called feature/new-login. Meanwhile, your teammate has been busy on the main branch, adding some crucial bug fixes.

Here’s the state of your repository before integration:

* abc1234 (feature/new-login) Implement user authentication
* def5678 Add validation to login form
|
* ghi9012 (main) Fix critical bug in payment processing
* jkl3456 Update dependency versions

You want to bring the changes from main into your feature/new-login branch.

Option 1: Git Merge

When you run git checkout feature/new-login followed by git merge main, Git creates a new "merge commit." This commit has two parents: the tip of your feature/new-login branch and the tip of the main branch.

The history now looks like this:

* mnp7890 (feature/new-login) Merge branch 'main' into feature/new-login
|\
| * abc1234 Implement user authentication
| * def5678 Add validation to login form
* | ghi9012 Fix critical bug in payment processing
* | jkl3456 Update dependency versions
|/

What it solves: merge is straightforward. It preserves the exact history of each branch, showing where and when the integration happened. It’s non-destructive, meaning it doesn’t rewrite existing commits.

How it works internally: Git finds a common ancestor commit between the two branches and then creates a new commit that combines the changes from both branches since that ancestor.

Levers you control:

  • --no-ff: Forces Git to create a merge commit even if the merge could be a fast-forward. This is useful for keeping feature branches distinct in the history.
  • --squash: This option pulls all the changes from the source branch into your current branch as a single commit, but it doesn’t create a merge commit. It’s a hybrid approach, often used when you want to combine many small commits into one logical unit before merging.

Option 2: Git Rebase

If you run git checkout feature/new-login and then git rebase main, Git does something quite different. It rewrites your feature/new-login branch. It takes the commits you made on feature/new-login (abc1234 and def5678), temporarily sets them aside, updates your feature/new-login branch to match main, and then replays your commits on top of the latest main.

The history now looks like this:

* uvw0123 (feature/new-login) Implement user authentication
* xyz4567 Add validation to login form
* ghi9012 (main) Fix critical bug in payment processing
* jkl3456 Update dependency versions

Notice how the feature/new-login commits now appear after the main commits, and their commit SHAs have changed because they are new commits. The original commits (abc1234, def5678) are effectively lost (though recoverable for a while).

What it solves: rebase creates a linear, cleaner history. It makes it look as though you developed your feature sequentially on top of the latest code, simplifying the commit log and making it easier to follow the project’s progression.

How it works internally: Git finds the common ancestor, rewinds your branch to that ancestor, applies the changes from the target branch (main in this case), and then reapplies your branch’s commits one by one onto the new base.

Levers you control:

  • --onto <new_base> <old_base>: This powerful option lets you move a series of commits from one branch to another. For example, git rebase --onto main develop~3 develop would take the last 3 commits from develop and reapply them onto main.
  • --interactive (or -i): This is where rebase truly shines. It allows you to rewrite your commit history before or during the rebase. You can reorder commits, combine multiple commits into one (squash), split a commit, edit commit messages, or even drop commits entirely.

The most common pitfall with rebase is misunderstanding that it rewrites history. You should never rebase a branch that has already been pushed to a shared remote repository and that other people might be working on. Doing so forces everyone else to manually reconcile their local history with your rewritten history, which is a painful process. This is why the golden rule is: "Only rebase your own unpushed commits."

Once you’ve mastered merge and rebase, you’ll likely want to explore strategies for managing merge conflicts more effectively.

Want structured learning?

Take the full Git course →