Version control is a team sport. This manual walks you through the rhythm a team shares, the experiments that reveal why the rules exist, and the questions that test whether it stuck.
Shared Phases
5
Experiments
3
Decisions
3
Mastery Qs
8
β¦
The CVS Engineer asks
I already know version control. Why do you keep calling this a team sport?
The Guide Replies
CVS had a server that owned history and working copies that borrowed from it. One repository, many readers β a broadcast. Git gives every clone the full repository, and GitHub adds a layer of structured collaboration on top β pull requests, reviews, required checks. What changes isn't the version-control theory, it's the social protocol. The rules below describe a rhythm a whole team moves to: the timing, the hand-offs, the recovery plays.
Part I β The Shared Rhythm
Every teammate moves through the same five phases.
Not in lockstep β at any moment different people are in different phases. But the phases are shared vocabulary. Click any node to inspect it.
Phase 01 β Sync
Start from an updated main.
Pull the latest state of the shared branch before you do anything else. Branching from stale main is the single largest source of avoidable merge pain. Exp. 02 Β· See the cost
This is the closest analog to cvs update, but you're also refreshing every branch and tag β not just the files you have checked out.
$ git switch main
$ git pull # or: git fetch, if you prefer review
Phase 02 β Branch
Make a new branch for every task.
In CVS, branching hurt β you avoided it. In Git, a branch is a 41-byte file pointing at a commit. Creating and deleting them is effectively free, so the right default is always branch.
Even one-line fixes go on a branch. This keeps main always deployable and makes code review possible. Exp. 01 Β· Run it
$ git switch -c feature/add-rate-limit
# you are now on a new branch; nothing has been published yet
Phase 03 β Build
Commit small & often. Push early.
Write code. Stage hunks intentionally with git add -p. Commit in coherent units with messages that explain the why. Push your branch to GitHub β it costs nothing, gives you an off-machine backup, and triggers CI.
This is the phase where the local/remote split really pays off: you can commit a dozen times, reshape them, and only publish a clean sequence.
A PR is a proposed merge plus a review thread. It's meant to be iterated on. Expect follow-up commits in response to review comments β that's how the mechanic works, not a sign of failure.
The PR is "done" when reviewers approve and CI is green β not when you open it.
$ gh pr create --fill
# opens a PR; CI starts running; reviewers get pinged
Phase 05 β Merge
Merge, delete, return to Phase 01.
Once approved and green, the PR merges into main. Your branch has served its purpose. Delete it on GitHub (usually automatic) and locally. Then pull main again β which is Phase 01 of the next turn.
The loop is closed. Short-lived branches are the whole point.
$ gh pr merge --squash --delete-branch
$ git switch main && git pull
# back to the top of the loop
The phases matter most when they're out of sync. While you're in Build, a teammate is opening a Propose, another is coming out of Merge. Shared phases are how everyone stays legible to each other.
β¦
Part II β Distinctive Mechanics
Three moves that look familiar but behave differently in a team.
Each of these is a mechanic you think you understand from CVS. Each has a twist that becomes visible once more than one person is involved.
Mechanic 01
The commit / push split.
In CVS, commit meant "publish." In Git it does not. These are two separate decisions β and the gap between them is where most of Git's power lives.
Below, your local repository is on the left and the shared remote (GitHub) is on the right. Watch what happens when you commit versus when you push.
Your Machine
local Β· feature/login
a1b2c3Initial commit
GitHub
origin Β· feature/login
a1b2c3Initial commit
Notice: committing fills up your local history without touching the remote. The remote only changes on push. That gap β between committing and publishing β is what lets you experiment freely, rewrite sloppy commits into a clean sequence, and work offline for days.
The CVS Engineer asks
Why would I want commits no one else can see? That just sounds like work that isn't saved.
Reply
They are saved β on your machine, with full history. The question reveals a CVS assumption: that "saved" and "shared" are the same event. Git decouples them. Keep that gap open for as long as you need: commit every 15 minutes, reshape the history into three clean commits, and then publish. The remote only ever sees your best work.
Mechanic 02
The staging area (a.k.a. the index).
CVS had two places a file could live: on disk and in the repo. Git has three: working tree β index β repository. That middle layer is doing real work.
Imagine you've been coding for an hour. You have changes across four files, but they split into two conceptual changes: a bug fix and a refactor. In CVS, either you commit them together (muddled history) or you manually revert half your work, commit, and redo it. In Git, you stage hunks one at a time. Click a hunk below to move it through the phases.
Working Tree
Staged (Index)
Committed
The skill to internalize: a commit should represent one coherent change, even when your working tree contains several in progress. git add -p is how you achieve this. Good commits make git log readable and git bisect useful; sloppy commits make both useless. Exp. 03 Β· Try sizes
Mechanic 03
Branches are free.
In CVS a branch was a server-side event with real overhead. In Git a branch is a text file containing a commit hash. The cost structure is so different that the strategy is different.
The consequence: branch speculatively. Try an idea on a branch. If it works, open a PR. If it doesn't, delete the branch β the commits will be garbage-collected and you've lost nothing. Long-lived branches are the anti-pattern; short-lived branches are the whole point.
β¦
Part III β Vocabulary
CVS verbs don't map one-to-one.
The words collide in confusing ways. Here's the translation table. Click a row for a note on the trap.
CVS habit
Git equivalent
Watch out for
cvs checkout <module>
git clone <url>
You get the full history, not just HEAD.
cvs update
git pull (= fetch + merge/rebase)
Consider git fetch alone β it's safer because nothing merges into your branch yet.
cvs commit
git commit + git push
Two distinct steps. This is the whole trick.
cvs tag
git tag (annotated)
Immutable refs. Annotated tags carry a message, author, and signature.
(branching β painful)
git branch / git switch -c
Now trivial. Change the strategy: branch freely, delete freely.
(per-file rev numbers)
whole-repo SHA commits
No file-level version numbers exist. Every commit is a snapshot of the whole tree.
(server conflict at commit)
local conflict at merge/rebase
Resolution happens on your machine, before you push. The server never sees your mess.
"the repository"
"a remote" (you can have many)
origin is just the default name. You can add upstream, fork, deploy β as many as you want.
β¦
Part IV β Run the Experiment
Change one thing at a time and watch the team's life change with it.
The rules aren't arbitrary. Each one is a response to a parameter getting bigger or smaller. Move the sliders below and see which rule suddenly becomes non-negotiable.
Exp. 01
What happens as the team grows?
Hold everything else constant. Change only the number of engineers pushing to the same repo.
1 engineer
131030
Collisions per day~0
Main stays deployableYes
Workflow you'll needPush direct to main
At 1 engineer, there are no collisions because nobody else is writing. You can push directly to main without hurting anyone. This is why solo projects often feel like Git doesn't need rules.
Exp. 02
How often should the team sync from main?
Hold team size and commit rate constant. Change only the interval between git pull operations on your feature branch.
1 hour
1h1 workday1 week2 weeks
Commits drifted from main~2
Expected conflict hunks~0
Merge painTrivial
At hourly sync, your branch tracks main almost perfectly. Rebases are no-ops; conflicts are rare and local. This is the cost structure Phase 01 of the loop assumes.
Exp. 03
How big should a single commit be?
Hold total work constant β you're going to write 2,000 lines either way. Change only how many lines land per commit.
~50 lines
~10~50~250~1000
git bisect steps to find a bug~8
Review effort per commitLow
Revert surface areaSmall
At ~50 lines per commit, each commit is one coherent change that a reviewer can hold in their head. git bisect converges quickly. Reverting one bad commit doesn't threaten neighboring work. This is the sweet spot git add -p is built for.
Every Git rule is a knob turned to a value that was painful at a smaller one. The experiments show the pain that made the knob necessary.
β¦
Part V β Decision Points
Three forks where the right move is non-obvious.
Experienced Git users recognize these by the trouble that follows picking the wrong one. Here's what each choice costs and when it's right.
I.
When you sync your branch β merge or rebase?
You're halfway through a feature branch. main has advanced. You need the new commits. You have two ways to pull them in, and they produce visibly different histories.
Default
git pull (merge)
Creates a new "merge commit" joining your branch to main. Preserves exact history but clutters git log with tiny merge nodes every time you sync.
Recommended
git pull --rebase
Replays your branch's commits on top of the new main. History stays linear. No noise. Your commits get new SHAs but preserve their content.
Rule of thumb: set pull.rebase = true globally. You get a linear feature history that reads like a story, not a tangle. The exception: long-lived branches shared with others (release branches, integration branches). For those, rebasing rewrites history that other people have already pulled β use explicit git merge instead. Exp. 02 Β· Divergence cost
The CVS Engineer objects
Rebase rewrites history. That sounds like exactly the kind of thing you told me to never do.
Reply
Good instinct β but scope matters. Rewriting history that only exists on your machine is private bookkeeping. Rewriting history others have pulled breaks their clones. Rebasing your own unpushed feature branch is the first kind; force-pushing a shared branch is the second. Keep rebase on private branches and you'll never break anything.
II.
Contributing to a repo β fork or branch?
You want to propose a change. Do you fork the repo first, or just make a branch?
Use when
Fork
A fork is a server-side clone under your own GitHub account. You push to your fork and open PRs against the upstream repo.
Use for: open-source contributions, any repo where you don't have write access, or when you want permanent working space under your own name.
Use when
Branch
A branch lives inside the shared repo. You push the branch to origin and open a PR within the same repo.
Use for: repos where you're a collaborator. No fork to keep synced, no remote juggling. Simpler in every way.
Rule of thumb: if you have write access, branch. If you don't, fork. Never fork inside your own team's repo "to be safe" β you'll just end up with two copies to keep synced.
III.
Undoing a bad commit β reset or revert?
You committed something you shouldn't have. The fix depends entirely on whether you've already pushed.
Local only
git reset
Moves the branch pointer backwards. The unwanted commits are discarded (or, with --soft, kept as uncommitted changes).
Never reset commits you've already pushed to a shared branch.
Safe anywhere
git revert
Creates a new commit whose diff is the inverse of the one you're undoing. History is preserved; the unwanted change is neutralized.
Always safe on public history.
Rule of thumb: before push β reset. After push β revert. The moment commits leave your machine, they become part of other people's clones, and rewriting them is a hostile act.
β¦
Part VI β When the Team Wins
What a clean turn looks like β together.
A feature loop the team has executed well ends with all of the following true at once. Any of them missing is a signal to tighten the rhythm.
β
Main advanced by one squashed commit
Your feature branch collapsed cleanly into main. One line in the log, one coherent change, one clear commit message.
β
CI passed before merge
Every status check on the PR was green. The protected-branch rule made this non-negotiable β you couldn't have merged without it.
β
At least one human approved
A reviewer read the diff and said yes. The PR thread recorded the exchange, so the reasoning survives.
β
The feature branch is gone
Deleted on GitHub (automatic after merge) and locally (git branch -d). No stale branches left behind.
β
You never touched main directly
Every commit reached main through a PR. main has no orphan work, no surprises, no "quick fixes" that skipped review. Exp. 01 Β· Why
β
The next turn begins from a fresh syncgit switch main && git pull. Back to Phase 01. The loop continues.
β¦
Part VII β Clever Gambits
Four tricks that make Git fun.
These are the moves that separate competent users from fluent ones. Memorize the names; reach for them when the moment arrives.
01
Surgical staging
git add -p
Walk through your working tree hunk by hunk, deciding per change what goes into the next commit. Messy work becomes clean commits. This is the feature that makes the staging area actually worth having. Exp. 03 Β· Why it matters
02
The time machine
git reflog
Every movement of HEAD for the last ~30 days, including ones you thought you destroyed. Reset too far? Deleted the wrong branch? Botched a rebase? The reflog has the SHA. Learn it before you need it.
03
The careful shove
--force-with-lease
A force-push that refuses to run if the remote moved since your last fetch. It's the difference between "I'm overwriting history" and "I'm overwriting exactly the history I think is there." Make it your default; plain --force should feel uncomfortable.
04
The bug hunt
git bisect
Binary-search your history to find the commit that introduced a bug. Mark a good commit, a bad commit, and Git checks out the midpoint for you to test. Repeat. In logβ(n) steps you've pinpointed the culprit. This is why good commit messages pay dividends.
The reflog is not a safety feature you turn on. It's the river the repository flows through β always running, always recording. You just have to know it's there.
β¦
Part VIII β Mastery Questions
Can you call the play when the pressure's on?
Eight scenarios, one correct move each. The wrong answers aren't strawmen β each is a plausible mistake an experienced engineer might make. Pick what you'd actually do. The debrief tells you why.
Answered
0 / 8
Correct
0
β¦
The team takes a breath.
The CVS Engineer concedes
Alright. I've had worse Mondays in CVS, and there was no reflog to save me.
The Guide Replies
That's the real shift. In CVS, a mistake was often terminal β the authoritative state lived on one server and you had borrowed a piece of it. In Git, every clone is a full backup of history and the reflog is a backup of your backup. The learning curve is steeper β but the floor is higher. Once you've committed something, it's genuinely hard to lose it. The team gets safer, not scarier.
β¦
Part IX β The Field Guide
Four references for the long walk to fluency.
This manual covers the shape of Git. The resources below give you the reps β the graph poked, animated, played with, and searched. Each comes with a note from our traveler on where your CVS instincts still help and where they mislead.
The CVS Engineer asks
Alright. The model is cleaner and the floor is higher. Where do I actually go to get fluent? I don't want another command reference β I want someone to walk me through the graph until I see it.
The Guide Replies
Four resources, each lighting up a different face of the graph β an interactive sandbox, a set of animations, a playful comic, and a hands-on guide to the command that actually changes how you debug. Below, the CVS engineer annotates each one: the mental shift it asks of you, the analogy that made it click, and where old instincts still help versus hinder.
01Β·Interactive Sandbox
Analogy
Learn Git Branching
Peter Cottle
An interactive sandbox with guided levels. You type real Git commands into a terminal and watch the commit DAG animate above. Covers commits, branches, merge, rebase, cherry-pick, interactive rebase, and remote tracking. The rebase levels in particular show commits being replayed β new hashes, same contents β which makes the idea concrete in a way no static diagram does.
If the DAG talk puts you off β mine did β try this instead. Commits are train cars. Parent pointers are the couplings between them. A branch is the locomotive at the front, pulling the cars along.
A merge is two trains joining at a junction: the merge commit is a single car with couplings going back to both parents. A rebase is uncoupling your cars, sliding them to a different track, and re-coupling behind a new locomotive. The cars look identical from the outside β same cargo, same diffs β but underneath they are new cars with new SHAs. That is exactly why rebasing a branch others are already riding breaks their tickets. An interactive rebase is the switching yard: before the train leaves, you can uncouple, reorder, drop, or weld cars together.
The metaphor strains a bit for rebase --onto and collapses for octopus merges, but it holds for the 90% of daily usage that matters β and it gives rebase the physical intuition most explanations lack.
02Β·Animated Walkthrough
Principle
CS Visualized: Useful Git Commands
Lydia Hallie
Each Git operation gets an animated GIF showing the graph transforming. The section on interactive rebase walks through all six actions β pick, reword, edit, squash, fixup, drop β each with its own animation. Watching a messy history of six "WIP" and "fix typo" commits visibly collapse into two clean commits is the most persuasive thirty seconds you will spend on the topic.
My first objection β and probably yours β was reasonable: history is a record, so why would I falsify it? I'd watched enough CVS admins corrupt ,v files to know what tampering looks like.
The reframe that landed: Git effectively keeps two histories. A private workshop where I develop something β false starts, debug prints, "WIP" β and the public record of what got shipped. Interactive rebase is how I clean up the first before publishing the second. A reviewer's actual question is "what does this change do?", and an eight-commit chain of fix typo β actually fix typo β address review β WIP forces them to reconstruct the real change from the noise. Two or three commits each doing one coherent thing answers directly.
The iron rule: only rewrite commits you have not yet shared. Rewriting published history is exactly the admin-edits-,v-behind-users'-backs scenario β same chaos, same reason it's forbidden.
03Β·Illustrated Primer
Quick Reference
Pick. Squash. Drop. Rebase!
Erika Heidi β a short comic
A short illustrated comic framing interactive rebase as a card game: commits are cards in your hand, and the rebase is laying them on the table to combine pairs, discard duds, and reorder before playing. Pairs naturally with the train-car metaphor β the cards are what's happening inside the switching yard.
The six interactive-rebase actions, one line each.
pick β keep this commit as is (the default).
reword β keep the change, rewrite the commit message.
edit β stop here so you can amend the actual contents.
squash β merge this commit into the one above; you'll be prompted to combine the messages.
fixup β like squash, but throw this message away.
drop β discard this commit entirely.
In a typical pre-review cleanup I mostly reach for pick, fixup, and reword. squash and edit come up occasionally. drop is for the debugging printf I forgot to remove.
04Β·Hands-On Tutorial
Why It Works
A Beginner's Guide to git bisect
Metal Toad
Walks through a fresh repo so you can follow along hands-on β git bisect start, mark endpoints, test, mark good or bad, repeat. Also covers git bisect run with an automated test script, which is what turns bisect from a neat trick into a routine debugging tool.
This is the command that convinced me the graph model was worth the trouble. In CVS, every file had its own ,v file with its own revision numbers β "revision 27 of the project" wasn't a coherent question. You'd be asking "was the build broken when foo.c was at 1.27 and bar.c was at 1.09?" and there was simply no first-class answer mapping that question to anything you could check out.
In Git, a commit is a whole-tree snapshot. Checking out commit X puts the entire repository into a specific, reproducible state. Commits form a DAG with well-defined ancestry. That's what makes binary search over history meaningful at all β you're searching an ordered sequence of atomic whole-project states, not a cross product of per-file revision numbers.
Once that clicks, the mechanics are almost embarrassingly simple: mark a known-bad endpoint (usually HEAD), mark a known-good one (a tag, a release), and Git checks out the midpoint for you to test. You say good or bad; it narrows. Repeat ~logβ(N) times; it prints the first bad commit. Hand git bisect run a test script that exits 0 for good and nonzero for bad, and it does all of that unattended. This is what the Linux kernel maintainers actually use on 30 million lines of code β it's a genuine superpower that the CVS era could not have given you.
The biggest conceptual shift from CVS is not the commands but the data model. CVS is a file-version system with project coordination layered on. Git is a directed graph of whole-project snapshots with commands that walk and rewrite that graph. Every operation that feels confusing β rebase, bisect, cherry-pick, reflog, detached HEAD β becomes obvious once you hold the graph in your head and stop translating it back into per-file revision numbers.