Branches, merging and rebasing
Most of the difficulty here comes from one thing being explained as though it were complicated. A branch is a file containing the name of a commit. Everything else follows from that, including why rebasing is dangerous in exactly one situation and harmless in all the others.
What a branch is
A pointer, and nothing more than a pointer.
A commit records the state of your project and the commit that came before it. Follow those links back and you have the history. Nothing in a commit knows anything about branches.
A branch is a small file holding the name of one commit, the tip. When you commit, git makes the commit and moves the pointer forward. That is the whole mechanism.
Three things stop being mysterious once you know that:
- Making a branch is instant and free, whatever the size of the project. It writes about forty bytes.
- Deleting a branch deletes almost nothing. It removes the pointer. The commits are still there, unreachable rather than gone, and the reflog will find them for about a month.
- Two branches can point at the same commit, which is exactly the state you are in immediately after making one.
git switch -c new-thing # make one and move to it
git switch main # move back
git branch -d new-thing # delete the pointer, refuse if unmerged
git branch -D new-thing # delete it anyway
git switch and git restore arrived in git 2.23 to split up the jobs git checkout used to do. git checkout -b still works and is the same thing.
Merging
Two shapes, and git picks for you.
A fast forward happens when nothing has been committed to the branch you are merging into since you left it. There is nothing to reconcile, so git slides the pointer forward and no merge commit is created. The history comes out as one straight line, as though you had worked on the main branch all along.
A merge commit happens when both branches have moved. Git works out a combined state and records a commit with two parents, one for each side. The history keeps the fact that the work happened separately.
git switch main
git merge new-thing
# Force a merge commit even where a fast forward was possible,
# so that the branch is visible in the history afterwards
git merge --no-ff new-thing
Whether you want fast forwards is a taste question and teams argue about it. The argument for --no-ff is that it keeps a record of which commits belonged to which piece of work, which is worth having when you are trying to revert a whole feature six weeks later.
Rebasing
The same work, told as if it happened later.
Rebasing takes your commits, sets them aside, moves your branch to the current tip of the other branch, and applies your work again on top. The result is a straight line with no merge commit.
git switch new-thing
git rebase main
Your commits are not moved. They are replaced. Every rebased commit is a new commit with a new identifier, because the thing it sits on top of has changed and a commit's identity includes its parent. The old ones become unreachable.
That single fact is the whole of the risk, and it is why the rule in the next section exists.
Used well, rebasing is how a branch stays current with main without collecting a merge commit every day, and how a messy sequence of eleven commits becomes three that somebody could review:
# Tidy the last five commits before anybody sees them:
# reorder, reword, squash together, or drop
git rebase -i HEAD~5
The one rule
Do not rewrite history that other people already have.
Rebasing, amending and hard resetting all replace commits rather than adding to them. On your own branch, before anybody has pulled it, that is tidying up and it is welcome.
Once somebody else has those commits, replacing them means their history and yours disagree about work they already have. Their next pull tries to reconcile two versions of the same thing, they get conflicts in code they never touched, and the usual resolution is somebody re-committing work that had already been re-committed. On a shared main branch this is how an afternoon disappears.
The test is not whether the branch is called main. It is whether the commits have left your machine in a form somebody else could have taken. A pushed feature branch that a colleague is reviewing counts.
If a shared branch does need something undone, revert it. Reverting adds a commit and upsets nobody.
Conflicts
Not an error. A question git cannot answer on its own.
A conflict means both sides changed the same lines and git will not guess. It stops, marks the file, and waits. Nothing is broken and nothing is lost.
git status # which files are waiting for you
Inside a conflicted file you get three markers. Above the middle divider is what was already on the branch you are merging into. Below it is what is coming in. Both are real code that somebody meant.
<<<<<<< HEAD
$timeout = 30;
=======
$timeout = 60;
>>>>>>> new-thing
Edit the file so it says what you want, which is often neither side exactly. Delete all three marker lines. Then:
git add the-file.php
git merge --continue # or git rebase --continue
To back out entirely and be exactly where you started:
git merge --abort
git rebase --abort
Search your project for the marker before committing. A conflict marker left in a file is one of the most common ways broken code reaches a live server, because the file is valid enough to save and not valid enough to run.
grep -rn "<<<<<<<" .
In a rebase, the sides swap over
The conflict markers say the opposite of what you expect.
This catches nearly everybody once. During a merge, the top half is your branch. During a rebase, git is replaying your commits on top of the other branch, so the top half is the other branch and the bottom half is your own work.
Resolve a rebase conflict on autopilot with merge habits and you will throw away the change you were trying to keep. When in doubt, stop and look at what the two sides actually say rather than trusting the labels.
A working answer
What to do if your team has no policy
- Rebase your own branch onto main while you are working, to stay current without collecting merge commits.
- Tidy up before review, not after. Interactive rebase is for commits nobody has read yet.
- Merge into main rather than rebasing onto it, so main only ever gains commits and never has any replaced.
- Never rewrite main. Not once, not quickly, not to fix a message.
- Delete branches after merging. The commits stay; the clutter does not.
That gives a readable history, keeps the dangerous operations on branches that only you have, and means nobody has to think about any of this on the branch that gets deployed.
Short answers
Questions people actually ask
What actually is a branch?
A small file containing the name of one commit. That is the whole mechanism, and it explains the rest: making a branch is instant whatever the size of the project, and deleting one removes a pointer rather than any commits.
Should I merge or rebase?
A workable default with no team policy: rebase your own branch onto main while you work, and merge into main rather than rebasing onto it. That keeps the history readable and keeps every history rewriting operation on a branch only you have.
Is rebasing dangerous?
Only in one situation, and then very. Rebasing replaces commits rather than moving them, so doing it to commits somebody else already has leaves two versions of the same work and gives them conflicts in code they never touched. On your own unpushed branch it is just tidying up.
What do the conflict markers mean?
Above the divider is what was already on the branch you are merging into. Below it is what is arriving. Both are real code somebody meant. Edit the file so it says what you want, which is often neither side exactly, delete all three marker lines, then add and continue.
Why does the conflict look backwards during a rebase?
Because it is. A rebase replays your commits on top of the other branch, so the top half is the other branch and the bottom half is your own work, which is the opposite of a merge. Resolving a rebase conflict with merge habits is how people throw away the change they were trying to keep.
I have made a mess of a merge. How do I start again?
git merge --abort, or git rebase --abort. Either one puts you back exactly where you were before it started. Nothing is lost and there is no penalty for using them.
Does deleting a branch delete the commits?
No. It removes the pointer and the commits become unreachable, which is not the same as gone. The reflog will find them for about a month.
More guides
Pushing, pulling and the errors you will hit
Rejected pushes, force with lease, and how to leave a detached HEAD.
What an exposed repository gives away
Every version of every file, including the secrets you took out again.
How to block it
The rule for Apache, nginx, Caddy, LiteSpeed, IIS and Cloudflare.