Advanced Scenarios: Managing a Multi-Branch Workflow
"Real projects are messy. Stash your work, resolve your conflicts, and keep moving."
By now you know the fundamentals. But real-world development is rarely linear — you'll be mid-feature when a critical bug drops, your branch will diverge from a teammate's, and two files will clash during a merge. These advanced tools handle the chaos.
5.1 "I Need to Switch Branches, but My Work Isn't Ready"
You're deep in a feature branch with ten modified files when your manager says "urgent bug on main." You can't commit half-finished work, and you can't lose it either. The stash is your escape hatch.
The stash is a temporary, private holding area for your dirty changes.
# 1. Stash your changes (-u includes brand-new files)
git stash push -u -m "WIP: refactoring pipeline, AI changes"
# 2. Fix the urgent bug
git switch main
git pull
git switch -c hotfix/urgent-bug
# ... fix, test, commit, push, create PR ...
# 3. Return to your work
git switch feature/A
git stash popgit stash only saves changes to tracked files. Brand-new files the AI just created — never
committed, never staged — get left behind in the working tree. Add -u (--include-untracked) to take them along — that's also why VS Code's menu
has a separate "Stash (Include Untracked)" item.git stash pop re-applies your changes and removes them from the stash. Use git stash apply to keep the stash entry for reuse.
In VS Code, you can do all of this without memorizing commands. Open the ... menu in the Source Control panel -- you'll see a Stash submenu with all the options you need:
Choose "Stash (Include Untracked)" to save all your work. When you're ready to come back, go to ... → Stash → "Pop Latest Stash" to restore everything exactly where you left off.
Try It: The Stash Workflow
feature/A when a critical bug comes in. Stash your work,
fix the bug on a hotfix branch, then come back and pop the stash.Loading playground...
"I need to switch branches but I'm not done here — save my work temporarily"
"Stash my current changes, switch to main to fix a bug, then come back and restore them"
5.2 "My Branch is Out of Date" (Rebase vs. Merge)
Your feature branch has been alive for a few days and main has moved on without you. Now you need to catch up — and Git offers two philosophies with very different trade-offs.
main has moved on. There are two philosophies for updating it.You have two options for catching up with main, and each tells a different story in your commit history.
git merge main
Creates a new "Merge Commit" on your branch. Preserves the exact history -- messy but 100% accurate.
History: "Worked on feature... merged main... worked on feature..."
git rebase main
"Replays" your commits on top of the latest main. Creates a clean, linear history as if you started today. The replayed commits are re-created — same changes, brand-new commit ids.
History: main's commits, then copies of yours (the ' marks: same change, new id) — all in a straight line.
VS Code supports both approaches. Use the ... menu in Source Control → "Pull (Rebase)" to rebase instead of merge when pulling. For merging, use the Command Palette (Cmd+Shift+P / Ctrl+Shift+P) → "Git: Merge Branch..." and select the branch to merge.
git push gets rejected. The correct follow-up is git push --force-with-lease — the safe force from section 4.6 — and it's only OK because this is your branch. Rebase,
lease-push, open the PR: that's the full ritual.One config gem while you're here: git config --global rebase.autostash true makes Git stash your uncommitted work automatically before a rebase (or pull --rebase) and pop it after — dissolving the "Git blocked my switch"
problem 5.1 opened with, for the rebase case at least.
Try It: Merge vs. Rebase
main have diverged. Try git merge main first, then reset and try git rebase main to compare the resulting history.Loading playground...
"My branch is behind main — rebase my changes on top of the latest main"
"Update my feature branch with the latest changes from main using rebase"
Rebase has one more trick: git rebase -i (interactive) lets you rewrite your own branch's history commit by commit —
squash five "wip" commits into one, reword a sloppy message, drop an experiment entirely.
It's how a messy working branch becomes a clean, reviewable PR. The Golden Rule applies
double here: only ever on commits that haven't been shared.
Try It: Squash the WIP
wip: commits. Run git rebase -i main, reword the first commit to something worthy, and squash
the other two into it.Loading playground...
"Squash my wip commits on this branch into one commit with a proper message"
"Clean up this branch history with an interactive rebase before I open the PR"
5.3 "We Both Edited the Same File" (Merge Conflicts)
This is the moment every developer dreads the first time — and handles calmly by the tenth. Two people changed the same lines, and Git needs a human to decide which version wins.
git pull (or merge main into your branch) and Git halts with CONFLICT. You and a teammate edited the same lines. Git needs you, the
human, to resolve it.Don't panic -- conflicts look intimidating at first, but they follow a simple pattern. Git inserts special markers into your file to show you exactly where the disagreement is.
The Conflict Markers
<<<<<<< HEAD
x = 10
# AI refactor
=======
x = 5
# teammate fix
>>>>>>> mainEverything between <<<<<<< HEAD and the ======= divider is your side (here, the AI's
refactor on your branch). Everything below the divider is the incoming side — the label after >>>>>>> names where it came from — the branch you merged (like main here; after a pull it can be a commit id or a longer label instead).
Delete all the markers (<<<, ===, >>>) and edit the code to be the correct final version, then stage and
commit. In the playground, you can write the resolved file with echo 'x = 10' > src/model.py.
Not ready to deal with it? There's an eject button here too: git merge --abort cancels the merge and returns your branch to exactly how it was before you ran git merge — the same guilt-free escape you'll meet again with rebase (5.5) and
cherry-pick (5.4).
The VS Code Way (The Superior Way)
Editing conflict markers by hand works, but VS Code makes the whole process much more visual and less error-prone.
Left Pane: "Incoming" (teammate's changes)
Right Pane: "Current" (your changes)
Bottom Pane: "Result" (what will be saved)
For complex conflicts with multiple overlapping changes, click "Resolve in Merge Editor" to open the full 3-way view. This gives you the most control over the final result:
Try It: Resolving a Merge Conflict
src/model.py. Use echo to overwrite the file, then git add and git commit to finish.Loading playground...
"I have a merge conflict in model.py — help me resolve it, keeping both changes"
"Show me the conflicts and suggest the best resolution for each one"
5.4 Cherry-Pick — Take Only the Gems
Your AI experiment branch is a mess — half-finished rewrites, abandoned TODOs, dead ends. But buried in the middle is one brilliant commit: a currency rounding fix that actually works. You don't want the branch. You want that one commit.
git cherry-pick copies one commit's changes onto your current branch as a new commit — same change, same message, but
a brand-new hash, because it now sits on a different parent. The original commit stays untouched
on its branch.
git log --oneline --all # Find the gem's hash on the experiment branch
git switch main # Stand on the branch that should receive it
git cherry-pick e4f5a6b # Copy exactly that commit here
git log --oneline # The fix is on main — the junk is notFor an audit trail, add -x: it appends "(cherry picked from commit
e4f5a6b...)" to the message, so anyone reading main later can trace where the fix came from.
Perfect for the reject-the-PR workflow below.
When to prefer it over merging: merge when the whole branch is worth keeping; cherry-pick when only part of it is. And because a cherry-picked commit replays changes onto code that may have moved on, conflicts can happen here too — the escape hatches mirror rebase exactly:
# Fix the conflicted file, then:
git add <file>
git cherry-pick --continue
# Or walk away as if nothing happened:
git cherry-pick --abortTry It: Cherry-Pick the Gem
experiment branch has a half-finished dashboard rewrite and one gem: a currency
rounding fix. Use git log --oneline --all to find it, then git cherry-pick it onto main — and check src/billing.py to confirm the fix arrived.Loading playground...
"The experiment branch is mostly junk but the rounding fix is good — cherry-pick just that commit onto main"
"Find the commit that fixed the login bug on the old branch and apply only that one here"
5.5 When Rebase Goes Wrong — Conflicts, Continue, Abort
Section 5.2 sold you on rebase for clean history — but it skipped the scary part. Your
feature branch tuned src/config.py, main changed the same lines, and halfway
through the rebase Git slams the brakes. Here's how to read the wreck and drive out of it.
git rebase main and Git stopped with CONFLICT. The rebase is half-done and you're not sure whether to fix it or
flee.Remember what rebase does: it replays your commits one at a time on top of the latest main. If a replayed commit touches lines that
main also changed, Git can't guess the winner — so it pauses mid-replay and hands you the keys. Run git status to see the paused state: it reports a rebase in progress and lists the conflicted files under "unmerged paths". The files contain the
conflict markers you learned to read in section 5.3 — with one crucial twist.
main and replaying your commits onto it — so <<<<<<< HEAD is main's version, and the bottom block is your own commit arriving as the "incoming" change. Exactly backwards from a merge.
This is the single most famous rebase trap — read the labels, not the positions.<<<<<<< HEAD
TIMEOUT = 10 # main's version (HEAD during a rebase!)
=======
TIMEOUT = 120 # your commit, being replayed
>>>>>>> a1b2c3d (feat: raise the worker timeout)From here, it's a three-step ritual — the same one every time:
# 1. Fix the file — remove the markers, keep the right code
echo 'TIMEOUT = 120' > src/config.py
# 2. Tell Git the conflict is resolved
git add src/config.py
# 3. Resume the replay
git rebase --continueIf more commits remain, Git keeps replaying — and may pause again on the next conflict. Just repeat the ritual until the rebase completes. And if at any point you're lost, confused, or late for dinner, there's a guilt-free eject button:
git rebase --abort # Everything returns EXACTLY as it was before the rebase--abort is the reason rebasing your local branches is always safe to attempt. The rebase
doesn't touch your original commits until it finishes — abort at any pause and your branch is
restored exactly as it was. The Golden Rule from 5.2 still stands (never rebase shared branches),
but on your own branch, the worst case is typing one command and being back where you started.git config --global rerere.enabled true turns on reuse recorded resolution — Git remembers how you resolved each conflict and
replays the answer the next time the identical conflict appears. On a long-lived AI branch that
gets rebased onto main every day, this is the difference between resolving the agent's conflict
once and resolving it every single morning.Try It: Survive a Rebase Conflict
feature/tuning branch raised the timeout in src/config.py, but main lowered the same line. Run git rebase main,
then cat src/config.py to read the conflict markers. Fix the file with echo, then git add and git rebase --continue. Try git rebase --abort too — watch everything snap back.Loading playground...
"My rebase stopped on a conflict in config.py — resolve it keeping the higher timeout and continue"
"This rebase is a mess — abort it and get my branch back to how it was"
5.6 Tags & Releases — Naming Your Milestones
Branches move — every commit drags them forward. But some moments deserve a permanent name: the commit you shipped, the version that passed the audit, the last known-good state before a big AI refactor. That's what tags are: labels that stick to one commit, forever.
Git has two kinds of tags, and the choice matters more than it looks:
Lightweight: git tag v1.1.0
Just a name pointing at a commit. No author, no date, no message. Fine for private bookmarks.
Annotated: git tag -a v1.1.0 -m "..."
A full object with author, date, and message. Always annotate releases — future-you will want to know who cut it and why.
Naming convention: most projects use semantic versioning — MAJOR.MINOR.PATCH. Bump MAJOR when you break existing users (v1.x → v2.0.0), MINOR when you add features compatibly (v1.0 → v1.1.0), and PATCH for pure bug fixes (v1.1.0 → v1.1.1).
One glance at a version tells users how scared to be.
git tag -a v1.1.0 -m "Release: import support" # Tag HEAD
git tag # List all tags
git log --oneline # Tags appear as decorations
git show v1.0.0 # Inspect what a release points atOne surprise: in real Git, git push does not send tags. You push them
explicitly — git push origin v1.1.0 for one tag, or git push origin --tags for all of them — though beware: --tags throws every tag at the remote, including private bookmarks like the pre-refactor checkpoint below. git push --follow-tags is the discerning version: it sends only annotated tags on commits you're pushing —
which is exactly why this section told you to annotate releases and keep bookmarks
lightweight. Need to remove a tag? git tag -d v1.1.0 deletes it locally — and if you deleted one by mistake, just re-tag the same commit
(find it with git log --oneline, or fetch the tag back from the remote if you'd already
pushed it).
git tag -a pre-refactor -m "Last known good before agent rewrite".
If things go sideways, a named restore point beats scrolling through the reflog trying to
remember which HEAD@{n} was the good one.Try It: Cut a Release
git tag -a v1.1.0 -m "Release: import support", list your tags,
then use git show v1.1.0 and git log --oneline to see what each release
points at.Loading playground...
"Tag the current commit as v2.0.0 with a message summarizing what's in this release"
"Before you start the refactor, create an annotated tag so we can get back to this exact state"
Loading challenge...