Merge Conflict Resolution Discipline
Conflicts happen. Resolution patterns.
Rebase
Rebase rewrites your branch's history so that your changes appear to have been made on top of the latest main, in a clean linear sequence. The result is a history that looks like everyone worked one after another, with no branching. For trunk-based development, where the main branch is the only long-lived line and short-lived feature branches collapse into it, rebase is the right default.
What rebase buys you and what it costs:
- Linear history.: The git log reads top to bottom as a sequence of changes, no merge commits cluttering the timeline. Bisecting to find when a regression appeared is much faster on a linear history than on a branchy one. For services with thousands of commits a year, this matters.
- Cleaner blame.: Each line in the codebase has a clear authorship trail. Merge commits in a tangled history obscure who changed what when. Rebase keeps the blame surface clean and useful.
- Default for trunk-based development.: If your main branch represents "the latest deployable version" and feature branches only exist for hours to days, rebase is the natural fit. The branch's purpose is to converge with main; rebase makes that convergence cheap and routine.
- Force-push required.: Rebasing a branch you have already pushed requires force-push, which is fine on personal branches but dangerous on shared branches. Teams using rebase enforce "no shared feature branches" to keep this safe.
- Conflict resolution one commit at a time.: When you rebase, conflicts surface one commit at a time. Each conflict is small and contextual. The downside is that for a 30-commit branch you might resolve the same kind of conflict 30 times. Squashing the branch first usually makes more sense.
Rebase is the right default for most modern engineering teams because their workflows are trunk-shaped. The teams that resist it are usually carrying a long-lived branch model that is itself the cause of their merge conflict pain.
Merge
Merge preserves the branch as a unit, with a merge commit recording the moment two histories joined. The history is non-linear but it captures the actual narrative of how work was done: this branch existed, these commits happened on it, this is when it merged back. For long-lived feature branches and for codebases that value historical fidelity, merge is the right default.
- Preserves branch context.: The merge commit's parent links record both sides of the merge. You can always reconstruct what was on the branch and when it merged. Code archaeology is easier on history that captures structure rather than smoothing it away.
- Default for feature branches that live for weeks.: If your branch is going to exist for two weeks of focused work before merging, the merge commit is more honest than a rebase. The branch was real; the history reflects that.
- Single conflict resolution.: Merge resolves all conflicts at once, in a single act, captured in the merge commit. For a branch with many small commits diverging from a main that has also moved, this is often less work than rebase.
- No force-push required.: Once merged, the branch's commits are immutable. Anyone who based work on the branch can keep doing so. For teams that share branches across multiple developers, this is a significant safety property.
- History gets noisier.: Many branches with many merge commits produces a history that is harder to read at a glance. Tools like `git log --first-parent` help, but the underlying graph is still complex. The cost of merge is paid every time someone reads the log.
Merge is the right default when the branch represents a meaningful unit of work whose existence is worth preserving. It is the wrong default when the branch is just "I made some changes and now I am pushing them up."
Avoid
The single biggest cause of painful conflicts is letting them age. A conflict caught when a branch is one day old is usually a 5 minute fix. The same conflict on a branch that has been festering for two weeks is a multi-hour archaeology project. The discipline is to merge often and rebase often, not to defer.
- Resolve conflicts fast.: The moment a conflict appears between your branch and main, deal with it. Do not wait until the end of the week, do not wait until the PR is "ready." Rebasing or merging main into your branch daily keeps conflicts small and contextual.
- Stale branches are harder.: A branch that has been off main for two weeks has accumulated 100 commits of drift. When you finally try to integrate, you are reasoning about a different codebase than the one your branch was based on. The fix is not better merge tooling; it is shorter-lived branches.
- Small PRs.: A 50 line PR has one conflict possibility per file changed. A 5,000 line PR has 100. Smaller PRs land faster, conflict less, and review better. They are the single biggest lever on merge pain.
- Pull main daily.: Engineers should rebase or merge main into their branch at least once a day. The cost is a few minutes; the savings is whatever the conflict would have grown into. Make it part of the morning ritual.
- Don't pile up unmerged branches.: A team with 30 open feature branches is a team that will spend more time on conflict resolution than on feature work. The right ratio is roughly one open branch per active engineer at any time.
- Conflict-prone areas get owners.: Some files (configuration, lock files, generated code) attract conflicts because everyone touches them. Designate one engineer to own merging changes to those files, or refactor them so multiple PRs do not collide on the same lines.
Merge conflicts are not a tooling problem. They are a workflow problem caused by branches that live too long and PRs that get too large. Nova AI Ops watches branch age and PR size as engineering health metrics, surfaces the patterns that are causing repeat conflicts (specific files, specific teams), and helps the team see when their working rhythm is feeding the merge pain instead of fighting it.