Merge or rebase? What changed when I stopped thinking only about conflicts
For a long time, almost my entire Git workflow revolved around `git pull`.
When I was building a feature and needed to bring my branch up to date with the latest changes from `develop`, I would usually pull `develop` into my branch. If conflicts appeared, I would resolve them, create a commit with the fixes, and only then open the Pull Request.
In practice, I was using Git effectively enough, but I was not giving much thought to how that integration was being recorded in the history.
That approach worked while the amount of simultaneous work was smaller. The problem became clearer as the team’s development flow grew: more developers meant more Pull Requests, more changes entering `develop`, and more features reaching integration with a considerable number of conflicts.
In some cases, that meant more time spent resolving integrations, delayed deployments, and even new validation work after conflicts had been fixed.
That was when I decided to study what really happens when a branch is updated, especially the difference between `merge` and `rebase`.
The main lesson was not discovering which one “avoids more conflicts.” It was realizing that I had been framing the problem incorrectly.
`merge` and `rebase` do not exist only to resolve conflicts between branches. They are, above all, different ways of **building and organizing Git history**.
---
The problem did not start in the Pull Request
Imagine a common situation. `develop` looks like this:
A---B---C
I create a branch for a feature:
A---B---C
\
D---E
While I work on commits `D` and `E`, another developer finishes a feature and its changes enter `develop`:
A---B---C---F---G
\
D---E
My feature started at `C`, but the main development branch is already at `G`.
My old tendency would be to update the branch with `git pull`, resolve any conflicts, create a commit for those fixes, and proceed to the PR.
The issue was not that this always caused trouble. It was that, without understanding the integration strategy in use, I was treating Git as a sequence of commands needed to “keep the branch updated,” rather than as a system for controlling history.
Depending on the changes in both features, that may be perfectly fine. But it can also produce integrations that are harder to understand, extra commits used only for synchronization, and Pull Requests that take more effort to review.
So the question changed from:
> “How do I resolve conflicts when I get to the PR?”
to:
> “When does it make sense to bring `develop` into my feature, and what history do I want to produce?”
That sounds like a small shift, but it changed the way I think about Git.
---
What actually happens in a merge
`merge` combines two lines of development while preserving their existing history.
Starting from the same scenario:
A---B---C---F---G
\
D---E
If I am on my feature branch and run:
git fetch origin
git merge origin/develop
Git can create a merge commit:
A---B---C---F---G
\ \
D---E---M
That commit explicitly represents the integration of the two histories. Commits `D` and `E` remain the same commits, as do `F` and `G`.
Git is not trying to pretend that one line of work happened after the other. It records that both lines existed in parallel and were combined at a given point.
This preserves the real development history very well. The trade-off is that, depending on the team’s workflow, many merges can make the graph more branched and harder to follow visually.
That does not make `merge` bad. It simply prioritizes a specific quality: **preserving branch history as it actually happened**.
---
What actually happens in a rebase
`rebase` follows a different strategy.
A---B---C---F---G
\
D---E
If my feature is mine alone, I can run:
git fetch origin
git rebase origin/develop
Git takes the commits from my feature and reapplies them on top of the current version of `develop`:
A---B---C---F---G---D'---E'
Visually, it looks as if my feature had always started after `G`. But that is not what actually happened.
`D'` and `E'` are new commits. Even when their changes are equivalent to `D` and `E`, their hashes are different. That is why we say rebase **rewrites history**.
This produces a linear history that can make reading, debugging, and understanding the sequence of changes easier. But it also introduces an important rule: rewriting history can be a problem when other people are already working on top of those commits.
---
My main rule became: is this history only mine?
This was probably the criterion that simplified my decision the most.
Before deciding between `merge` and `rebase`, I now ask:
**Is this feature being developed only by me, or is this history shared?**
When I am working alone on a feature, I tend to use `rebase` to update its base after `develop` receives relevant changes. Rewriting my feature’s commits usually affects no one but me.
git fetch origin
git switch feature/my-feature
git rebase origin/develop
After that, I keep working on top of the latest `develop`. If conflicts exist, they appear during the rebase and can be solved before the final Pull Request.
The goal is not to eliminate conflicts. It is to **bring integration forward**.
---
Rebase is not a “remove conflicts” button
It is easy to assume that rebasing often means fewer conflicts. But rebase has no special ability to prevent two people from changing the same part of a file.
If two branches change the same lines in incompatible ways, there will be a conflict regardless of the chosen strategy.
The difference is how it appears. With merge, I am generally integrating two complete histories. With rebase, Git reapplies my commits one by one on a new base.
Depending on the sequence of commits, I may even need to solve conflicts more than once during a rebase.
The improvement in my workflow came from somewhere else: updating the feature before the PR allowed me to discover incompatibilities earlier. Instead of concentrating all complexity at the end of the task, integration becomes part of development itself.
---
I do not rebase for every new commit in develop
After understanding the benefits of rebase, it would be easy to go to the opposite extreme: a new commit enters `develop`, so I rebase; another PR is accepted, so I rebase again.
In practice, I do not see a need for that.
I generally rebase when there are meaningful changes between features or when I am getting close to integration. If another developer changed an area that is unrelated to my work, there is not necessarily a reason to interrupt development just to reorganize the branch.
The same is true for very short features. Git should not become a sequence of rituals executed automatically. Each operation needs a purpose.
---
Shared features are different
The situation changes considerably when two people are working on the same feature.
develop
A---B---C
\
D---E---F
I have `D`, `E`, and `F` locally, and another developer has them as well.
If I rebase those commits on a newer `develop`, I may end up with:
A---B---C---G---H---D'---E'---F'
That can look valid from my perspective, but my colleague still has:
D---E---F
We now have two versions of the same development. The changes may be similar, but the commit identifiers are no longer the same.
In this scenario, I prefer to preserve shared history. A merge brings in the latest `develop` without replacing commits that someone else is working on:
git fetch origin
git switch feature/shared
git merge origin/develop
The extra history is an acceptable cost when the alternative is rewriting commits shared by several people.
---
Updating a feature and merging a PR are separate decisions
There are two different problems:
1. How my feature keeps up with changes in `develop`. 2. How that feature is incorporated into `develop` at the end.
Those decisions do not need to use the same strategy.
I can update an individual feature with:
git rebase origin/develop
and later have the Pull Request integrated through the strategy chosen by the team: a merge commit, a squash, or another repository policy.
This helped me stop thinking of “merge or rebase” as one decision applied to the entire workflow. There are several moments of integration, and each can have different needs.
---
Sometimes I need neither
I create a feature from `develop`, work on it, and finish it. During that period, no relevant change enters `develop`.
In that case, my branch is still based on exactly the expected state. There is nothing to synchronize.
I do not need to merge simply because I am about to open a PR, and I do not need to rebase as a mandatory step. If the base has not changed, there is no new base on which to replay my commits.
This sounds obvious once the mechanism is clear, but I used to treat some Git operations as fixed process steps instead of tools used when there was a concrete need.
---
What changed in my workflow
After this study, my practical rule is roughly:
- an individual feature and meaningful changes in `develop`: I consider `rebase`; - a feature shared with another developer: I preserve the history and use `merge` when we need to integrate `develop`; - `develop` has not changed since the feature began: I do neither out of habit.
I do not consider this a universal rule. The best strategy depends on the team, project size, and repository policies.
The most important change was starting to understand **why** I am performing each operation.
---
The lesson went beyond resolving conflicts
The study started because we were facing more integration problems: more Pull Requests meant more conflicts, more time resolving merges, and sometimes more validation after integration.
At first, I simply wanted a better way to reduce those problems. The result was deeper than that.
I came to understand that `merge` and `rebase` are not merely two ways to update a branch. They express decisions about a project’s history.
`merge` explicitly preserves two lines of development. `rebase` reorganizes history to present commits on a new base.
Neither strategy is automatically better. The right choice depends on the context.
Today, my question is no longer:
> “Which command creates fewer conflicts?”
It is:
> “Is this history shared, how do I want to integrate it, and is there really anything that needs synchronization?”
That shift in perspective is what improved my use of Git the most. I still have details to learn, but no longer treating `merge`, `rebase`, and even `pull` as tools for putting out fires at PR time has made my workflow more predictable and reduced some of the integration complexity I used to encounter.