A feature branch starts clean. It branches off main on Monday, adds tax to the receipt total, and by Wednesday main has moved on: someone rounded the totals to two decimals and added a README. The branch still works, but merging it back adds a merge commit that joins two lines of history, a fork and a join for work that could read as a straight line. Rebasing is the other way to bring a branch up to date. It lifts the branch's commits off the old base and replays them on top of the latest main, so the history reads top to bottom. The question is when that straight line is worth the rewriting it takes.

What I would check before using it

My judgement, from years of working in shared codebases, is this: rebase a branch only while it is yours alone, and merge once anyone else depends on it. That single check decides almost every rebase question. If the branch exists only on your machine, rebasing is safe, because nobody else has the old commits and nobody will notice they were rewritten. If the branch is pushed and a teammate has pulled it, rebasing rewrites commits they already have, and their next pull will fail or duplicate the work. In that case, merge instead, and accept the merge commit as the honest record that two lines of work joined.

I would check three things first. Is the branch pushed, and has anyone else pulled it? If yes to either, do not rebase. Second, how many commits are on the branch? A branch with one or two commits rebases quickly, and a conflict affects at most one or two of them. A branch with twenty commits can stop for a conflict on every one of them, and a branch that long should have been merged or split sooner. Third, is your working tree clean? Git refuses to rebase with uncommitted changes, so commit or stash first, or the rebase will not even start. The verdict, in short: rebase short, private branches to keep history readable, and merge shared branches, long branches, and anything you are unsure about. If a rebase goes wrong halfway through, git rebase --abort returns the branch to exactly where it was before you started, so a bad rebase is recoverable as long as you have not pushed the rewritten commits anywhere.

A worked example that is not a toy

A small shop script, receipt.py, totals the day's orders. It starts on main with one commit, then a feature branch adds a 17 percent tax to the total while main moves ahead with two commits of its own. Rebase does not change what the code does, only where its commits sit: a merge keeps both lines of history and records the join, while a rebase replays each commit on top of the latest main, one at a time, so the history comes out as a single straight line. If a commit touches lines main has also changed, Git stops and asks you to resolve the conflict, exactly as a merge would. Here is the whole session, run for real with git 2.43.0. The script starts like this:

def total(items): return sum(items) if __name__ == "__main__": print(total([120.50, 45.00, 9.99])) 

The branch feature/tax changes the return line to return sum(items) * 1.17. Meanwhile main changes the same line to return round(sum(items), 2) and adds a README. Before the rebase, the history forks:

* 28e3f78 Add 17 percent tax to total | * 8b54ca4 Add README | * c9d23ba Round totals to two decimals |/ * 9fb9514 Add receipt total script 

The branch has one commit sitting beside main's two commits. Now the rebase, from the feature branch:

git checkout feature/tax git rebase main 

Output:

Auto-merging receipt.py CONFLICT (content): Merge conflict in receipt.py error: could not apply 28e3f78... Add 17 percent tax to total 

Now the part the tidy tutorials skip: something goes wrong. Both branches changed the same line, so Git cannot decide which version wins and stops the rebase to ask. The file now holds both versions with conflict markers:

def total(items): <<<<<<< HEAD return round(sum(items), 2) ======= return sum(items) * 1.17 >>>>>>> 28e3f78 (Add 17 percent tax to total) 

The fix is to write the line that does both jobs, rounding the taxed total, and then tell Git the conflict is resolved:

# edit receipt.py so the line reads: # return round(sum(items) * 1.17, 2) git add receipt.py git rebase --continue 

Output:

[detached HEAD 89ef524] Add 17 percent tax to total 1 file changed, 1 insertion(+), 1 deletion(-) Successfully rebased and updated refs/heads/feature/tax. 

Notice the hash changed: the commit was 28e3f78 before the rebase and is 89ef524 after it. Same message, same change, new commit, because its parent is now the latest main instead of the old base. That is what rewriting history means in practice. The history after the rebase is a straight line:

* 89ef524 Add 17 percent tax to total * 8b54ca4 Add README * c9d23ba Round totals to two decimals * 9fb9514 Add receipt total script 

No fork, no merge commit. The tax commit sits on top of both main commits as if the branch had started from the latest main all along. And the script still works, with the tax and the rounding both in place:

$ python3 receipt.py 205.32 

The three orders come to 175.49, plus 17 percent tax is 205.3233, rounded to two decimals is 205.32. The rebase changed the history, not the result.

Common mistakes and how to fix them

1. Rebasing a branch someone else has already pulled. The symptom appears when you push: ! [rejected] feature/tax -> feature/tax (non-fast-forward) and error: failed to push some refs. The fix is git push --force-with-lease, which replaces the remote branch only if nobody has pushed to it since you last fetched. Never use a plain --force on a shared branch, and better still, do not rebase a shared branch at all.

2. Rebasing with uncommitted changes. The symptom is immediate: error: cannot rebase: You have unstaged changes. followed by error: Please commit or stash them. Git refuses to start while you have edits in progress. The fix is to commit first, or git stash the changes and git stash pop after the rebase.

3. Committing a file with conflict markers still in it. After resolving one conflict, it is easy to miss a second one further down the file. The symptom is code that will not run, or code that runs with a stray marker treated as text. The fix is to search the file for <<<<<<< before adding it, with grep -rn "<<<<<<<" ., and to read the resolved lines once more. If you have already committed the markers, fix the file and amend with git commit --amend, before pushing.

Where this leaves you

Rebase is a history tool, not a code tool: it moves commits, it does not change what they do. Use it on short branches that live only on your machine, and your history will read like a story instead of a map of forks. Use merge everywhere else, especially on anything shared, and let the merge commit record the join honestly. The next step is to take a branch you have not pushed yet, rebase it onto the latest main, and watch the graph straighten out. That is the moment rebase stops being an abstract Git concept and becomes a habit you reach for on purpose.

Further reading

Originally published at https://practicaldevblog.blogspot.com/2026/11/git-rebase-explained-straight-line.html