Skip to content

2. Branches and merging

Beginner → Intermediate · 14 min read

Branches let you try something (a new prompt, a different retriever, a streaming endpoint) without breaking the version that works. In Git they're also almost free, which is why every team workflow is built on them.

2.1 A branch is just a pointer

git branch
cat .git/HEAD
cat .git/refs/heads/main
Output
* main
ref: refs/heads/main
db0294fc226368196b4ee96a26a762f941d08020

That's the whole implementation: a branch is a file containing one commit hash. HEAD says which branch you're on. When you commit, Git writes the new commit and moves the current branch's pointer forward to it. Creating a branch writes one 41-byte file, so make as many as you like.

git switch -c feature/streaming        # create a branch here and switch to it
git switch main                        # switch back
git branch -a                          # list local and remote branches
git branch -d feature/streaming        # delete a merged branch (-D forces it)

git switch and git restore are the modern replacements for the overloaded git checkout, which did both jobs. You'll still see checkout in older guides.

2.2 Fast-forward merge

Add a stream() helper on a branch, then merge it into main:

git switch -c feature/streaming
# ...edit app.py...
git commit -am "Add streaming helper"
git switch main
git merge feature/streaming
Output
[feature/streaming e463ff6] Add streaming helper
 1 file changed, 4 insertions(+)
Switched to branch 'main'
Updating db0294f..e463ff6
Fast-forward
 app.py | 4 ++++
 1 file changed, 4 insertions(+)

main hadn't moved since the branch started, so there was nothing to combine. Git just slid the main pointer forward to e463ff6. That's a fast-forward: no new commit.

2.3 Three-way merge

Now both sides move: a with_retries() helper on a branch, and a README on main:

git switch -c feature/retries
# ...add with_retries() to app.py...
git commit -am "Add with_retries()"
git switch main
# ...add README.md...
git commit -m "Add README"
git merge feature/retries
git log --oneline --graph --all
Output
Merge made by the 'ort' strategy.
 app.py | 9 +++++++++
 1 file changed, 9 insertions(+)
*   c04f52b Merge branch feature/retries
|\  
| * daae83f Add with_retries()
* | 3f4e61f Add README
|/  
* e463ff6 Add streaming helper
* db0294f Add answer() helper

Git found the merge base (e463ff6, where the branches split), compared each side against it, combined the changes and made a merge commit with two parents. The changes touched different lines, so it worked automatically.

2.4 Merge conflicts

A conflict happens when both sides change the same lines. Here one branch sets TEMPERATURE = 0.0 for deterministic evals while main raises it to 0.7 for friendlier chat:

git merge tune/temp-low
git status --short
Output
Auto-merging app.py
CONFLICT (content): Merge conflict in app.py
Automatic merge failed; fix conflicts and then commit the result.
UU app.py

UU means "unmerged, changed on both sides". Git wrote both versions into the file between markers:

app.py
import os
import time

MODEL = os.getenv("MODEL", "claude-opus-5-5")
<<<<<<< HEAD
TEMPERATURE = 0.7   # friendlier chat
=======
TEMPERATURE = 0.0   # deterministic for evals
>>>>>>> tune/temp-low

The top half is yours (HEAD, the branch you're on), the bottom half is theirs (the branch being merged). Resolving means editing the file to what it should be. That's often neither side. Here the right answer is to make it configurable:

app.py (resolved)
MODEL = os.getenv("MODEL", "claude-opus-5-5")
TEMPERATURE = float(os.getenv("TEMPERATURE", "0.7"))   # evals set TEMPERATURE=0

Then mark it resolved and finish the merge:

git add app.py
git commit --no-edit
git log --oneline --graph -4
Output
[main fdc4cd2] Merge branch 'tune/temp-low'
*   fdc4cd2 Merge branch 'tune/temp-low'
|\  
| * 459509c Use temperature 0 for evals
* | bd61ced Raise temperature for chat
|/  

Conflict tips:

Situation Do
You want to give up and start again git merge --abort puts everything back
The file is generated (uv.lock, package-lock.json) Don't hand-edit it: take one side (git checkout --theirs uv.lock) and regenerate (uv lock)
Many conflicts Use your editor's merge view (VS Code shows "Accept current / incoming / both")
After resolving Search for leftover <<<<<<< markers, and run the tests before committing

Small branches merged often have small conflicts. A branch that lives for three weeks will hurt.

2.5 Rebase: replaying your commits on top

While you worked on feature/system-prompt, main got two new commits:

Before
* c992d23 Shorter answers
* 85be588 Add system prompt
| * 4def4ad Pin requests
| * b267a28 Add README
|/  
* 620a8dc Add app

Instead of merging main in, you can rebase: take your branch's commits and replay them on top of the latest main:

git switch feature/system-prompt
git rebase main
git log --oneline --graph --all
Output
Successfully rebased and updated refs/heads/feature/system-prompt.
* 9aa10a7 Shorter answers
* f6541b2 Add system prompt
* 4def4ad Pin requests
* b267a28 Add README
* 620a8dc Add app

History is now a straight line, and merging into main is a fast-forward. Look at the hashes: 85be588 became f6541b2. Rebase made new commits (new parents mean new hashes, see page 1), and the old ones are abandoned.

If a replayed commit conflicts, Git stops: fix the file, git add it, then git rebase --continue (or git rebase --abort to cancel).

Merge or rebase?

Merge Rebase
History True record, with merge commits and branches Straight line, easier to read and bisect
Rewrites commits No Yes (new hashes)
Safe on shared branches Always Only on commits nobody else has
Conflicts Resolved once Possibly once per replayed commit

The golden rule: never rebase commits that others have already pulled. Rebasing your own feature branch to catch up with main before opening a PR is fine and common. Rebasing main is not.

git rebase -i HEAD~3 (interactive) lets you squash, reorder, reword or drop your last commits before sharing them: a good way to turn "wip", "fix", "fix again" into one clean commit.

2.6 Cherry-pick: copy one commit

A release branch is frozen at an older commit, and you need just one bug fix from main:

git switch release/1.0
git cherry-pick main          # or a commit hash
git log --oneline
Output
[release/1.0 5270680] Fix: add request timeout
 Date: Thu Oct 1 10:00:00 2026 +0530
 1 file changed, 1 insertion(+)
5270680 Fix: add request timeout
4def4ad Pin requests
b267a28 Add README
620a8dc Add app

Cherry-pick applies that commit's changes as a new commit (new hash) on your current branch. Use it for hotfixes and backports, not as a substitute for merging.

Interview questions

What is a Git branch, technically?

A movable pointer: a file under .git/refs/heads/ containing the hash of the latest commit on that branch. HEAD points to the current branch. Committing creates a new commit and moves the current branch pointer to it, which is why branches are cheap to create.

Fast-forward vs three-way merge?

If the target branch hasn't moved since the feature branch split off, Git simply moves its pointer forward: a fast-forward, no new commit. If both moved, Git compares each side against their merge base, combines the changes and records a merge commit with two parents.

How do you resolve a merge conflict?

Open the conflicted files, read both sides between the <<<<<<<, ======= and >>>>>>> markers, edit the file to the correct final version (often a combination), remove the markers, run the tests, git add the files and commit. git merge --abort cancels. Generated files like lock files are regenerated, not hand-edited.

When should you use rebase instead of merge?

To update your own, not-yet-shared feature branch with the latest main, or to clean up your commits with interactive rebase before a PR, giving a linear history. Never rebase commits others have pulled, because rebase creates new commits and their copies would diverge. Merge is always safe and preserves the true history.

What does git cherry-pick do?

Applies the changes from one existing commit onto the current branch as a new commit. Typical use: backporting a bug fix from main to a release branch.

Practice

  • Create two branches that edit the same line, merge them, and resolve the conflict by hand.
  • Make three small commits on a branch, then squash them into one with git rebase -i HEAD~3.
  • Run git log --oneline --graph --all after each step to watch the pointers move.

Next: 3. Undoing changes Back to: Git & GitHub overview · Notes overview