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¶
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
[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
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:
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:
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:
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:
[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:
* 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:
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:
[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 --allafter each step to watch the pointers move.
Next: 3. Undoing changes Back to: Git & GitHub overview · Notes overview